diff --git a/packages/core-common/__tests__/remote-service/data-store/index.test.ts b/packages/core-common/__tests__/remote-service/data-store/index.test.ts new file mode 100644 index 0000000000..2db511ef45 --- /dev/null +++ b/packages/core-common/__tests__/remote-service/data-store/index.test.ts @@ -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 }); + }); +}); diff --git a/packages/core-common/__tests__/remote-service/data-store/select.test.ts b/packages/core-common/__tests__/remote-service/data-store/select.test.ts new file mode 100644 index 0000000000..d178efb508 --- /dev/null +++ b/packages/core-common/__tests__/remote-service/data-store/select.test.ts @@ -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 }]); + }); +}); diff --git a/packages/core-common/src/remote-service/README.md b/packages/core-common/src/remote-service/README.md index ac89425e1c..b2b7deeb53 100644 --- a/packages/core-common/src/remote-service/README.md +++ b/packages/core-common/src/remote-service/README.md @@ -223,6 +223,8 @@ export class OpenVsxExtensionManagerModule extends NodeModule { GDataStore 会实现默认的 CRUD 接口,让你使用它就像使用一个 MongoDB 数据库一样。 +这部分的思路来自 feathers,这是一个很有个性的后端框架,提供了非常方便的接口声明以及数据操作。 + 来看一个实际的场景,我们有一个全局的 TerminalService,它会监听 GDataStore(TerminalDataStore) 的 created/removed 事件,然后做相关处理。 ```ts @@ -230,12 +232,12 @@ interface Item { id: string; } -interface GDataStore { - create(item: Item): void; +export interface GDataStore { + create(item: T): void; find(query: Record): void; size(query: Record): void; - get(id: string, query?: Record): Item; - update(id: string, item: Partial): void; + get(id: string, query?: Record): T; + update(id: string, item: Partial): void; remove(id: string): void; } diff --git a/packages/core-common/src/remote-service/data-store/index.ts b/packages/core-common/src/remote-service/data-store/index.ts new file mode 100644 index 0000000000..84a8312b1b --- /dev/null +++ b/packages/core-common/src/remote-service/data-store/index.ts @@ -0,0 +1,82 @@ +import extend from 'lodash/extend'; + +import { EventEmitter } from '@opensumi/events'; + +import { select } from './select'; + +export interface DataStore { + create(item: Item): Item; + find(query: Record): Item[] | undefined; + size(query: Record): number; + get(id: string, query?: Record): Item | undefined; + update(id: string, item: Partial): void; + remove(id: string): void; +} + +export interface DataStoreEvent extends Record { + created: [Item]; + updated: [oldValue: Item, newValue: Item]; + removed: [Item]; +} + +export interface DataStoreOptions { + id?: string; +} + +export class InMemoryDataStore extends EventEmitter> implements DataStore { + private store = new Map(); + 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): Item[] | undefined { + return select(this.store, query); + } + + size(query?: Record): 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): 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); + } +} diff --git a/packages/core-common/src/remote-service/data-store/select.ts b/packages/core-common/src/remote-service/data-store/select.ts new file mode 100644 index 0000000000..1aa5ae1c47 --- /dev/null +++ b/packages/core-common/src/remote-service/data-store/select.ts @@ -0,0 +1,42 @@ +import { isIterable } from '@opensumi/ide-utils'; + +export type Query = Record; +export type Store = Iterable | Record | Map; + +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>(items: T, query: Query): I[] { + const matcher = makeMatcher(query); + const result = [] as I[]; + + let _iterable: Iterable | 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; +} diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index 04750128aa..868df6eb3a 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -238,3 +238,11 @@ export type RemoveReadonly = { export function removeReadonly(obj: T): RemoveReadonly { return obj as any; } + +export function isIterable(obj: any): obj is Iterable { + // checks for null and undefined + if (obj == null) { + return false; + } + return typeof obj[Symbol.iterator] === 'function'; +}