Skip to content

Commit

Permalink
feat: implement data store
Browse files Browse the repository at this point in the history
  • Loading branch information
bytemain committed Sep 26, 2024
1 parent a050054 commit 0f00cd3
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { InMemoryDataStore } from '../../../src/remote-service/data-store';

describe('InMemoryDataStore', () => {
it('should work', () => {
const users = [
{ user: 'barney', age: 36, active: true },
{ user: 'fred', age: 40, active: false },
{ user: 'pebbles', age: 1, active: true },
];

const store = new InMemoryDataStore<(typeof users)[0]>({
id: 'user',
});

let addCount = 0;
store.on('created', (i) => {
addCount++;
});

users.forEach((u) => {
store.create(u);
});

const userBarney = store.get('barney');
expect(userBarney).toEqual(users[0]);
expect(store.size()).toBe(3);
expect(addCount).toBe(3);

const items = store.find({ active: true });
expect(items).toEqual([users[0], users[2]]);
expect(store.size({ active: true })).toBe(2);

store.on('updated', (oldValue, newValue) => {
expect(oldValue.user).toBe('barney');
expect(oldValue.age).toBe(36);
expect(newValue.age).toBe(37);
});

store.update('barney', { age: 37 });
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import _ from 'lodash';

import { select } from '../../../src/remote-service/data-store/select';

describe('select', () => {
it('should work', () => {
const users = [
{ user: 'barney', age: 36, active: true },
{ user: 'fred', age: 40, active: false },
{ user: 'pebbles', age: 1, active: true },
];

const result = select(users, { age: 1, active: true });
expect(result).toEqual([{ user: 'pebbles', age: 1, active: true }]);

const userObj = _.keyBy(users, 'user');
const result2 = select(userObj, { age: 1, active: true });
expect(result2).toEqual([{ user: 'pebbles', age: 1, active: true }]);
});
});
10 changes: 6 additions & 4 deletions packages/core-common/src/remote-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,19 +223,21 @@ export class OpenVsxExtensionManagerModule extends NodeModule {

GDataStore 会实现默认的 CRUD 接口,让你使用它就像使用一个 MongoDB 数据库一样。

这部分的思路来自 feathers,这是一个很有个性的后端框架,提供了非常方便的接口声明以及数据操作。

来看一个实际的场景,我们有一个全局的 TerminalService,它会监听 GDataStore(TerminalDataStore) 的 created/removed 事件,然后做相关处理。

```ts
interface Item {
id: string;
}

interface GDataStore<T extends Item> {
create(item: Item): void;
export interface GDataStore<T extends Item> {
create(item: T): void;
find(query: Record<string, any>): void;
size(query: Record<string, any>): void;
get(id: string, query?: Record<string, any>): Item;
update(id: string, item: Partial<Item>): void;
get(id: string, query?: Record<string, any>): T;
update(id: string, item: Partial<T>): void;
remove(id: string): void;
}

Expand Down
82 changes: 82 additions & 0 deletions packages/core-common/src/remote-service/data-store/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import extend from 'lodash/extend';

import { EventEmitter } from '@opensumi/events';

import { select } from './select';

export interface DataStore<Item> {
create(item: Item): Item;
find(query: Record<string, any>): Item[] | undefined;
size(query: Record<string, any>): number;
get(id: string, query?: Record<string, any>): Item | undefined;
update(id: string, item: Partial<Item>): void;
remove(id: string): void;
}

export interface DataStoreEvent<Item> extends Record<string, any> {
created: [Item];
updated: [oldValue: Item, newValue: Item];
removed: [Item];
}

export interface DataStoreOptions {
id?: string;
}

export class InMemoryDataStore<Item> extends EventEmitter<DataStoreEvent<Item>> implements DataStore<Item> {
private store = new Map<string, Item>();
private _uId = 0;
private id: string;

constructor(protected options?: DataStoreOptions) {
super();
this.id = options?.id || 'id';
}

create(item: Item): Item {
const id = item[this.id] || String(this._uId++);
const result = extend({}, item, { [this.id]: id }) as Item;

this.store.set(id, result);

this.emit('created', result);
return result;
}

find(query: Record<string, any>): Item[] | undefined {
return select(this.store, query);
}

size(query?: Record<string, any>): number {
if (!query) {
return this.store.size;
}

return this.find(query)?.length || 0;
}

get(id: string): Item | undefined {
return this.store.get(id);
}

update(id: string, item: Partial<Item>): void {
const current = this.store.get(id);
if (!current) {
return;
}

const result = extend({}, current, item);
this.emit('updated', current, result);

this.store.set(id, result);
}

remove(id: string): void {
const item = this.store.get(id);
if (item) {
this.emit('removed', item);
}

this.store.delete(id);
}
}
42 changes: 42 additions & 0 deletions packages/core-common/src/remote-service/data-store/select.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { isIterable } from '@opensumi/ide-utils';

export type Query = Record<string, any>;
export type Store<T> = Iterable<T> | Record<string, T> | Map<string, T>;

function makeMatcher(query: Query) {
const statements = [] as string[];
Object.entries(query).forEach(([key, value]) => {
statements.push(`item['${key}'] === ${value}`);
});

const matcher = `
return ${statements.join(' && ')};
`;

const func = new Function('item', matcher) as (item: Query) => boolean;

return (item: Query) => func(item);
}

export function select<I, T extends Store<I>>(items: T, query: Query): I[] {
const matcher = makeMatcher(query);
const result = [] as I[];

let _iterable: Iterable<any> | undefined;

if (items instanceof Map) {
_iterable = items.values();
} else if (isIterable(items)) {
_iterable = items;
} else {
_iterable = Object.values(items);
}

for (const item of _iterable) {
if (matcher(item)) {
result.push(item);
}
}

return result;
}
8 changes: 8 additions & 0 deletions packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,11 @@ export type RemoveReadonly<T> = {
export function removeReadonly<T>(obj: T): RemoveReadonly<T> {
return obj as any;
}

export function isIterable<T = any>(obj: any): obj is Iterable<T> {
// checks for null and undefined
if (obj == null) {
return false;
}
return typeof obj[Symbol.iterator] === 'function';
}

0 comments on commit 0f00cd3

Please sign in to comment.