@cuboapp/crdt 1.0.26 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.d.ts +77 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/queue/index.d.ts +60 -0
- package/dist/client/queue/index.d.ts.map +1 -0
- package/dist/client/types/document.d.ts +23 -0
- package/dist/client/types/document.d.ts.map +1 -0
- package/dist/client/types/index.d.ts +60 -0
- package/dist/client/types/index.d.ts.map +1 -0
- package/dist/client/types/store.d.ts +17 -0
- package/dist/client/types/store.d.ts.map +1 -0
- package/dist/client/types/utils.d.ts +13 -0
- package/dist/client/types/utils.d.ts.map +1 -0
- package/dist/constants/index.d.ts +7 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/server/document/index.d.ts +50 -0
- package/dist/server/document/index.d.ts.map +1 -0
- package/dist/server/index.d.ts +77 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/types/document.d.ts +59 -0
- package/dist/server/types/document.d.ts.map +1 -0
- package/dist/server/types/index.d.ts +50 -0
- package/dist/server/types/index.d.ts.map +1 -0
- package/dist/server/types/subscribe.d.ts +35 -0
- package/dist/server/types/subscribe.d.ts.map +1 -0
- package/dist/server/utils/index.d.ts +3 -0
- package/dist/server/utils/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +23 -18
- package/src/client/index.ts +29 -19
- package/src/client/types/index.ts +0 -2
- package/src/server/index.ts +24 -13
- package/src/server/types/subscribe.ts +2 -2
- package/src/types/index.ts +11 -1
- package/src/utils/index.ts +5 -2
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { CuboCrdtKey, CuboCrdtRowId, CuboCrdtSubscriptionStatus } from '../types';
|
|
2
|
+
import { CuboCrdtClientDoc, CuboCrdtClientDocOrigin, CuboCrdtClientList, CuboCrdtClientOptions, CuboCrdtClientRow, CuboCrdtClientStore, CuboCrdtClientUseOptions } from './types';
|
|
3
|
+
export * from './types';
|
|
4
|
+
export declare class CuboCrdtClient<M> {
|
|
5
|
+
private opts;
|
|
6
|
+
store: CuboCrdtClientStore<M>;
|
|
7
|
+
docs: import("vue").ShallowReactive<Map<string, CuboCrdtClientDoc>>;
|
|
8
|
+
private listeners;
|
|
9
|
+
private subscriptions;
|
|
10
|
+
private onReconnect;
|
|
11
|
+
private onDisconnect;
|
|
12
|
+
private eventQueue;
|
|
13
|
+
constructor(opts: CuboCrdtClientOptions<M>);
|
|
14
|
+
start(): Promise<void>;
|
|
15
|
+
stop(): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Переподписка на все активные подписки после реконнекта.
|
|
18
|
+
*
|
|
19
|
+
* При обрыве сокета бэкенд удаляет клиента вместе со всеми его подписками
|
|
20
|
+
* (removeClient -> cleanSubscribe), а после реконнекта сокету выдаётся новый id.
|
|
21
|
+
* Локальные слушатели (this.listeners) и yjs-документы при этом сохраняются,
|
|
22
|
+
* поэтому достаточно заново отправить SUBSCRIBE по реестру — заново
|
|
23
|
+
* навешивать слушатели не нужно.
|
|
24
|
+
*/
|
|
25
|
+
private resubscribeAll;
|
|
26
|
+
private resolvePaginated;
|
|
27
|
+
/**
|
|
28
|
+
* Подписка на КОЛИЧЕСТВО строк под фильтрами.
|
|
29
|
+
*
|
|
30
|
+
* Возвращает реактивное число и живёт как обычная подписка: сервер пересчитывает
|
|
31
|
+
* count на любую мутацию сущности и присылает только totals — строки не грузятся,
|
|
32
|
+
* CRDT-документы не создаются.
|
|
33
|
+
*
|
|
34
|
+
* Зачем: счётчику (оранжевая точка инбокса, число задач в «Неразобранном») нужно одно
|
|
35
|
+
* число, а обычная подписка ради него держала бы весь набор строк — память на сервере,
|
|
36
|
+
* трафик на каждое их изменение и документы, которые никто не читает.
|
|
37
|
+
*
|
|
38
|
+
* const { count, unsubscribe } = crdt.useCount('events', { filters: { recipient: 5 } })
|
|
39
|
+
*/
|
|
40
|
+
useCount<K extends Extract<keyof M, string>>(entity: K | string, opts?: CuboCrdtClientUseOptions): {
|
|
41
|
+
count: import("vue").ComputedRef<number>;
|
|
42
|
+
subscribe_id: string;
|
|
43
|
+
storeKey: string;
|
|
44
|
+
subscribe: (filters?: any) => void;
|
|
45
|
+
upgrade: (filters?: any) => void;
|
|
46
|
+
unsubscribe: (clear?: boolean) => void;
|
|
47
|
+
subscribed: () => import("vue").ComputedRef<boolean>;
|
|
48
|
+
loading: () => import("vue").ComputedRef<boolean>;
|
|
49
|
+
ready: () => import("vue").ComputedRef<boolean>;
|
|
50
|
+
status: () => import("vue").ComputedRef<CuboCrdtSubscriptionStatus>;
|
|
51
|
+
error: () => import("vue").ComputedRef<unknown>;
|
|
52
|
+
totals: () => import("vue").ComputedRef<import("..").CuboCrdtListTotals>;
|
|
53
|
+
revision: () => import("vue").ComputedRef<number>;
|
|
54
|
+
rows: () => import("vue").ComputedRef<CuboCrdtKey<K, M>[]>;
|
|
55
|
+
rowsById: () => import("vue").ComputedRef<Record<string, CuboCrdtKey<K, M>>>;
|
|
56
|
+
};
|
|
57
|
+
useComputedList<K extends Extract<keyof M, string>>(entity: K): import("vue").ComputedRef<M[K][]>;
|
|
58
|
+
useComputedRow<K extends Extract<keyof M, string>>(entity: K, entity_id: CuboCrdtRowId): import("vue").ComputedRef<M[K]>;
|
|
59
|
+
useList<K extends Extract<keyof M, string>, T = CuboCrdtKey<K, M>>(entity: K | string, opts?: CuboCrdtClientUseOptions): CuboCrdtClientList<T>;
|
|
60
|
+
useDoc<K extends Extract<keyof M, string>>(entity: K, id: number): CuboCrdtClientDoc;
|
|
61
|
+
useRow<K extends Extract<keyof M, string>, T = CuboCrdtKey<K, M>>(entity: K | string, id: number, opts?: CuboCrdtClientUseOptions): CuboCrdtClientRow<T>;
|
|
62
|
+
update<K extends Extract<keyof M, string>>(entity: K, entity_id: CuboCrdtRowId, dto: K extends Extract<keyof M, string> ? Partial<M[K]> : object, opts?: CuboCrdtClientDocOrigin): void;
|
|
63
|
+
upgrade(subscribe_id: string, filters?: Record<string, any>): void;
|
|
64
|
+
private applyDocumentRows;
|
|
65
|
+
private attachDocument;
|
|
66
|
+
private detachDocument;
|
|
67
|
+
private applyRowJson;
|
|
68
|
+
private onDocumentCreate;
|
|
69
|
+
private onDocumentUpdate;
|
|
70
|
+
private onDocumentDelete;
|
|
71
|
+
private onIncomingUpdate;
|
|
72
|
+
private onListSync;
|
|
73
|
+
private onAwarenessUpdate;
|
|
74
|
+
private get ws();
|
|
75
|
+
private get debug();
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
type QueueErrorMode = 'continue' | 'stop';
|
|
2
|
+
export interface AsyncQueueOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Что делать, если задача упала:
|
|
5
|
+
* - continue: логируем/пробрасываем через onError и идём дальше
|
|
6
|
+
* - stop: останавливаем очередь (следующие задачи не стартуют)
|
|
7
|
+
*/
|
|
8
|
+
errorMode?: QueueErrorMode;
|
|
9
|
+
/**
|
|
10
|
+
* Хук на ошибки задач (если errorMode=continue — обязательно пригодится)
|
|
11
|
+
*/
|
|
12
|
+
onError?: (err: unknown) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Максимальный размер очереди (для защиты от утечек, опционально)
|
|
15
|
+
*/
|
|
16
|
+
maxSize?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class AsyncSerialQueue {
|
|
19
|
+
private running;
|
|
20
|
+
private stopped;
|
|
21
|
+
private readonly errorMode;
|
|
22
|
+
private readonly onError?;
|
|
23
|
+
private readonly maxSize?;
|
|
24
|
+
private tasks;
|
|
25
|
+
private drainWaiters;
|
|
26
|
+
constructor(opts?: AsyncQueueOptions);
|
|
27
|
+
get size(): number;
|
|
28
|
+
get isRunning(): boolean;
|
|
29
|
+
get isStopped(): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Добавляет задачу в очередь.
|
|
32
|
+
* Возвращает промис, который резолвится когда задача реально выполнена.
|
|
33
|
+
*/
|
|
34
|
+
enqueue(fn: () => void | Promise<void>): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Ждёт, пока очередь станет пустой и ничего не выполняется.
|
|
37
|
+
*/
|
|
38
|
+
drain(): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Очищает очередь (не прерывая текущую выполняемую задачу).
|
|
41
|
+
* Невыполненные задачи получат reject.
|
|
42
|
+
*/
|
|
43
|
+
clear(reason?: unknown): void;
|
|
44
|
+
/**
|
|
45
|
+
* Останавливает очередь: новые enqueue будут отвергнуты.
|
|
46
|
+
* Можно опционально очистить pending.
|
|
47
|
+
*/
|
|
48
|
+
stop(opts?: {
|
|
49
|
+
clear?: boolean;
|
|
50
|
+
reason?: unknown;
|
|
51
|
+
}): void;
|
|
52
|
+
/**
|
|
53
|
+
* Разрешает снова enqueue (не запускает автоматически).
|
|
54
|
+
*/
|
|
55
|
+
resume(): void;
|
|
56
|
+
private pump;
|
|
57
|
+
private notifyDrainedIfNeeded;
|
|
58
|
+
}
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/queue/index.ts"],"names":[],"mappings":"AAAA,KAAK,cAAc,GAAG,UAAU,GAAG,MAAM,CAAA;AAEzC,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,SAAS,CAAC,EAAE,cAAc,CAAA;IAE1B;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;IAEhC;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,OAAO,CAAQ;IAEvB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAwB;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAQ;IAEjC,OAAO,CAAC,KAAK,CAIN;IAEP,OAAO,CAAC,YAAY,CAAwB;gBAEhC,IAAI,GAAE,iBAAsB;IAMxC,IAAI,IAAI,WAEP;IAED,IAAI,SAAS,YAEZ;IAED,IAAI,SAAS,YAEZ;IAED;;;OAGG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBtD;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B;;;OAGG;IACH,KAAK,CAAC,MAAM,GAAE,OAAoC,GAAG,IAAI;IAOzD;;;OAGG;IACH,IAAI,CAAC,IAAI,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,IAAI;IAK5D;;OAEG;IACH,MAAM,IAAI,IAAI;YAKA,IAAI;IA4BlB,OAAO,CAAC,qBAAqB;CAO9B"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Awareness } from 'y-protocols/awareness';
|
|
2
|
+
import { Doc } from 'yjs';
|
|
3
|
+
import { CuboCrdtClientUseOptions } from '..';
|
|
4
|
+
import { CuboCrdtExposeStrategy } from '../../types';
|
|
5
|
+
import { CuboCrdtClientBaseOptions } from './utils';
|
|
6
|
+
export type CuboCrdtClientDoc = {
|
|
7
|
+
doc: Doc;
|
|
8
|
+
subscribes: Set<string>;
|
|
9
|
+
awareness?: Awareness;
|
|
10
|
+
awarenessSubscribes: Set<string>;
|
|
11
|
+
};
|
|
12
|
+
export type CuboCrdtClientDocOrigin = {
|
|
13
|
+
store?: boolean;
|
|
14
|
+
react?: boolean;
|
|
15
|
+
keys?: string[];
|
|
16
|
+
expose?: CuboCrdtExposeStrategy;
|
|
17
|
+
subscribe_id?: string;
|
|
18
|
+
};
|
|
19
|
+
export type CuboCrdtClientDocUpdateOptions = {
|
|
20
|
+
subscribe_id: string;
|
|
21
|
+
storeKey: string;
|
|
22
|
+
} & Pick<CuboCrdtClientBaseOptions, 'onBeforeCreate' | 'onAfterCreate' | 'onBeforeUpdate' | 'onAfterUpdate' | 'onBeforeDelete' | 'onAfterDelete'> & Pick<CuboCrdtClientUseOptions, 'awareness'>;
|
|
23
|
+
//# sourceMappingURL=document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../../../src/client/types/document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACjD,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,OAAO,EAAE,wBAAwB,EAAE,MAAM,IAAI,CAAA;AAC7C,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAEpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAA;AAEnD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,GAAG,CAAA;IACR,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACvB,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,mBAAmB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CACjC,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IAEpC,KAAK,CAAC,EAAE,OAAO,CAAA;IAGf,KAAK,CAAC,EAAE,OAAO,CAAA;IAGf,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IAGf,MAAM,CAAC,EAAE,sBAAsB,CAAA;IAG/B,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,8BAA8B,GAAG;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAC5F,yBAAyB,EACzB,gBAAgB,GAAG,eAAe,GAAG,gBAAgB,GAAG,eAAe,GAAG,gBAAgB,GAAG,eAAe,CAC7G,GACC,IAAI,CAAC,wBAAwB,EAAE,WAAW,CAAC,CAAA"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type WsClient } from '@cuboapp/ws';
|
|
2
|
+
import { ComputedRef } from 'vue';
|
|
3
|
+
import { CuboCrdtAction, CuboCrdtListSyncData, CuboCrdtListTotals, CuboCrdtSubscriptionStatus } from '../../types';
|
|
4
|
+
import { CuboCrdtClientBaseOptions } from './utils';
|
|
5
|
+
export * from './document';
|
|
6
|
+
export * from './store';
|
|
7
|
+
export * from './utils';
|
|
8
|
+
export type CuboCrdtClientOptions<M> = {
|
|
9
|
+
ws: WsClient;
|
|
10
|
+
entities: Extract<keyof M, string>[];
|
|
11
|
+
debug?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type CuboCrdtClientUseOptions = CuboCrdtClientBaseOptions & {
|
|
14
|
+
storeKey?: string;
|
|
15
|
+
filters?: Record<string, any>;
|
|
16
|
+
awareness?: boolean;
|
|
17
|
+
paginated?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Подписка только на КОЛИЧЕСТВО строк под фильтрами: строки не грузятся и документы
|
|
20
|
+
* не создаются, приезжает лишь totals. Для счётчиков и индикаторов — см. useCount.
|
|
21
|
+
*/
|
|
22
|
+
counted?: boolean;
|
|
23
|
+
};
|
|
24
|
+
export type CuboCrdtClientSubscribeEvent = {
|
|
25
|
+
action: CuboCrdtAction;
|
|
26
|
+
entity: string;
|
|
27
|
+
entity_id?: number;
|
|
28
|
+
subscribe_id?: string;
|
|
29
|
+
data?: number[] | CuboCrdtListSyncData;
|
|
30
|
+
};
|
|
31
|
+
export type CuboCrdtClientList<T> = {
|
|
32
|
+
subscribe_id: string;
|
|
33
|
+
storeKey: string;
|
|
34
|
+
subscribe: (filters?: any) => void;
|
|
35
|
+
upgrade: (filters?: any) => void;
|
|
36
|
+
unsubscribe: (clear?: boolean) => void;
|
|
37
|
+
subscribed: () => ComputedRef<boolean>;
|
|
38
|
+
loading: () => ComputedRef<boolean>;
|
|
39
|
+
ready: () => ComputedRef<boolean>;
|
|
40
|
+
status: () => ComputedRef<CuboCrdtSubscriptionStatus>;
|
|
41
|
+
error: () => ComputedRef<unknown>;
|
|
42
|
+
totals: () => ComputedRef<CuboCrdtListTotals>;
|
|
43
|
+
revision: () => ComputedRef<number>;
|
|
44
|
+
rows: () => ComputedRef<T[]>;
|
|
45
|
+
rowsById: () => ComputedRef<Record<string, T>>;
|
|
46
|
+
};
|
|
47
|
+
export type CuboCrdtClientRow<T> = {
|
|
48
|
+
subscribe_id: string;
|
|
49
|
+
storeKey: string;
|
|
50
|
+
subscribe: () => void;
|
|
51
|
+
upgrade: (filters?: any) => void;
|
|
52
|
+
unsubscribe: (clear?: boolean) => void;
|
|
53
|
+
subscribed: () => ComputedRef<boolean>;
|
|
54
|
+
loading: () => ComputedRef<boolean>;
|
|
55
|
+
ready: () => ComputedRef<boolean>;
|
|
56
|
+
status: () => ComputedRef<CuboCrdtSubscriptionStatus>;
|
|
57
|
+
error: () => ComputedRef<unknown>;
|
|
58
|
+
row: () => ComputedRef<T>;
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,KAAK,CAAA;AAEjC,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAElH,OAAO,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAA;AAEnD,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,SAAS,CAAA;AAEvB,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI;IACrC,EAAE,EAAE,QAAQ,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,CAAA;IACpC,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG,yBAAyB,GAAG;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,4BAA4B,GAAG;IACzC,MAAM,EAAE,cAAc,CAAA;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAAA;CACvC,CAAA;AAED,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAAI;IAClC,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAClC,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC,WAAW,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC,UAAU,EAAE,MAAM,WAAW,CAAC,OAAO,CAAC,CAAA;IACtC,OAAO,EAAE,MAAM,WAAW,CAAC,OAAO,CAAC,CAAA;IACnC,KAAK,EAAE,MAAM,WAAW,CAAC,OAAO,CAAC,CAAA;IACjC,MAAM,EAAE,MAAM,WAAW,CAAC,0BAA0B,CAAC,CAAA;IACrD,KAAK,EAAE,MAAM,WAAW,CAAC,OAAO,CAAC,CAAA;IACjC,MAAM,EAAE,MAAM,WAAW,CAAC,kBAAkB,CAAC,CAAA;IAC7C,QAAQ,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,CAAA;IACnC,IAAI,EAAE,MAAM,WAAW,CAAC,CAAC,EAAE,CAAC,CAAA;IAC5B,QAAQ,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAA;CAC/C,CAAA;AAED,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI;IACjC,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC,WAAW,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC,UAAU,EAAE,MAAM,WAAW,CAAC,OAAO,CAAC,CAAA;IACtC,OAAO,EAAE,MAAM,WAAW,CAAC,OAAO,CAAC,CAAA;IACnC,KAAK,EAAE,MAAM,WAAW,CAAC,OAAO,CAAC,CAAA;IACjC,MAAM,EAAE,MAAM,WAAW,CAAC,0BAA0B,CAAC,CAAA;IACrD,KAAK,EAAE,MAAM,WAAW,CAAC,OAAO,CAAC,CAAA;IACjC,GAAG,EAAE,MAAM,WAAW,CAAC,CAAC,CAAC,CAAA;CAC1B,CAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Reactive } from 'vue';
|
|
2
|
+
import { CuboCrdtKey, CuboCrdtListTotals, CuboCrdtSubscriptionStatus } from '../../types';
|
|
3
|
+
export type CuboCrdtClientStoreItem<T> = {
|
|
4
|
+
state: Reactive<{
|
|
5
|
+
rows: T[];
|
|
6
|
+
subscribed: boolean;
|
|
7
|
+
loading: boolean;
|
|
8
|
+
status: CuboCrdtSubscriptionStatus;
|
|
9
|
+
error?: unknown;
|
|
10
|
+
totals: CuboCrdtListTotals;
|
|
11
|
+
revision: number;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
export type CuboCrdtClientStore<M> = Partial<{
|
|
15
|
+
[K in string]: CuboCrdtClientStoreItem<CuboCrdtKey<K, M>>;
|
|
16
|
+
}>;
|
|
17
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/client/types/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAA;AAE9B,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAEzF,MAAM,MAAM,uBAAuB,CAAC,CAAC,IAAI;IACvC,KAAK,EAAE,QAAQ,CAAC;QACd,IAAI,EAAE,CAAC,EAAE,CAAA;QACT,UAAU,EAAE,OAAO,CAAA;QACnB,OAAO,EAAE,OAAO,CAAA;QAChB,MAAM,EAAE,0BAA0B,CAAA;QAClC,KAAK,CAAC,EAAE,OAAO,CAAA;QACf,MAAM,EAAE,kBAAkB,CAAA;QAC1B,QAAQ,EAAE,MAAM,CAAA;KACjB,CAAC,CAAA;CACH,CAAA;AACD,MAAM,MAAM,mBAAmB,CAAC,CAAC,IAAI,OAAO,CAAC;KAC1C,CAAC,IAAI,MAAM,GAAG,uBAAuB,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CAC1D,CAAC,CAAA"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Doc } from 'yjs';
|
|
2
|
+
import { CuboCrdtClientSubscribeEvent } from '.';
|
|
3
|
+
import { CuboCrdtClientDocUpdateOptions } from './document';
|
|
4
|
+
export type CuboCrdtClientBaseOptions = {
|
|
5
|
+
autoSubscribe?: boolean;
|
|
6
|
+
onBeforeCreate?: (update: Uint8Array, ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) => Promise<boolean> | boolean;
|
|
7
|
+
onAfterCreate?: (doc: Doc, update: Uint8Array, ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) => Promise<void> | void;
|
|
8
|
+
onBeforeUpdate?: (doc: Doc, update: Uint8Array, ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) => Promise<boolean> | boolean;
|
|
9
|
+
onAfterUpdate?: (doc: Doc, update: Uint8Array, ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) => Promise<void> | void;
|
|
10
|
+
onBeforeDelete?: (doc: Doc, ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) => Promise<boolean> | boolean;
|
|
11
|
+
onAfterDelete?: (doc: Doc, ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) => Promise<void> | void;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/client/types/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AACzB,OAAO,EAAE,4BAA4B,EAAE,MAAM,GAAG,CAAA;AAChD,OAAO,EAAE,8BAA8B,EAAE,MAAM,YAAY,CAAA;AAE3D,MAAM,MAAM,yBAAyB,GAAG;IACtC,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB,cAAc,CAAC,EAAE,CACf,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,4BAA4B,EACjC,IAAI,EAAE,8BAA8B,KACjC,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAA;IAC/B,aAAa,CAAC,EAAE,CACd,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,4BAA4B,EACjC,IAAI,EAAE,8BAA8B,KACjC,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAEzB,cAAc,CAAC,EAAE,CACf,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,4BAA4B,EACjC,IAAI,EAAE,8BAA8B,KACjC,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAA;IAC/B,aAAa,CAAC,EAAE,CACd,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,4BAA4B,EACjC,IAAI,EAAE,8BAA8B,KACjC,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAEzB,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,4BAA4B,EAAE,IAAI,EAAE,8BAA8B,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAA;IAClI,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,4BAA4B,EAAE,IAAI,EAAE,8BAA8B,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC5H,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/constants/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe;;;;;CAK3B,CAAA"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,SAAS,CAAA;AACvB,cAAc,SAAS,CAAA"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Awareness } from 'y-protocols/awareness';
|
|
2
|
+
import { CuboCrdtServerDocumentOptions, CuboCrdtServerDocumentOrigin } from '../types';
|
|
3
|
+
export declare class CuboCrdtServerDocument {
|
|
4
|
+
opts: CuboCrdtServerDocumentOptions;
|
|
5
|
+
private ydoc;
|
|
6
|
+
private persistedState;
|
|
7
|
+
private observedState;
|
|
8
|
+
private storeQueue?;
|
|
9
|
+
private pendingStore?;
|
|
10
|
+
private storeTimeout?;
|
|
11
|
+
awareness: Awareness;
|
|
12
|
+
awarenessBySubscribe: Map<string, Set<number>>;
|
|
13
|
+
constructor(opts: CuboCrdtServerDocumentOptions);
|
|
14
|
+
enableAwareness(): void;
|
|
15
|
+
get name(): string;
|
|
16
|
+
get stateAsUpdate(): Uint8Array<ArrayBufferLike>;
|
|
17
|
+
applyUpdate(update: Uint8Array, origin?: CuboCrdtServerDocumentOrigin): void;
|
|
18
|
+
getMap(): import("yjs").Map<unknown>;
|
|
19
|
+
getXmlFragment(name?: string): import("yjs").XmlFragment;
|
|
20
|
+
getJson(): {
|
|
21
|
+
[x: string]: any;
|
|
22
|
+
};
|
|
23
|
+
getPersistedState(): any;
|
|
24
|
+
setPersistedState(row: object): void;
|
|
25
|
+
write(dto: object, origin?: CuboCrdtServerDocumentOrigin): void;
|
|
26
|
+
get id(): string;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Немедленно передаёт trailing-пакет в очередь и ждёт все уже принятые
|
|
30
|
+
* сохранения документа. Используется перед вытеснением Y.Doc из серверного
|
|
31
|
+
* кэша, чтобы refresh последнего клиента не потерял его update.
|
|
32
|
+
*/
|
|
33
|
+
flushStoreQueue(): Promise<void>;
|
|
34
|
+
/** Есть ли принятое клиентское изменение в debounce или очереди записи. */
|
|
35
|
+
hasPendingStore(): boolean;
|
|
36
|
+
store(body: object, origin: CuboCrdtServerDocumentOrigin, row?: {
|
|
37
|
+
[x: string]: any;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Схлопывает только соседние изменения одного origin и одного набора
|
|
41
|
+
* leaf-путей. Смена пользователя или поля немедленно фиксирует предыдущий
|
|
42
|
+
* пакет, сохраняя фактический порядок collaborative-обновлений.
|
|
43
|
+
*/
|
|
44
|
+
debounceStore(body: object, origin: CuboCrdtServerDocumentOrigin): Promise<void>;
|
|
45
|
+
/** Перезапускает trailing debounce для текущего однородного пакета. */
|
|
46
|
+
private schedulePendingStore;
|
|
47
|
+
/** Передаёт накопленный пакет в общую последовательную очередь записи. */
|
|
48
|
+
private flushPendingStore;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/document/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAyB,MAAM,uBAAuB,CAAA;AAExE,OAAO,EACL,6BAA6B,EAC7B,4BAA4B,EAE7B,MAAM,UAAU,CAAA;AAKjB,qBAAa,sBAAsB;IAYd,IAAI,EAAE,6BAA6B;IAXtD,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,UAAU,CAAC,CAAe;IAClC,OAAO,CAAC,YAAY,CAAC,CAAoC;IACzD,OAAO,CAAC,YAAY,CAAC,CAA+B;IAC7C,SAAS,EAAE,SAAS,CAAA;IACpB,oBAAoB,2BAAiC;gBAIzC,IAAI,EAAE,6BAA6B;IA2C/C,eAAe;IA6BtB,IAAW,IAAI,WAEd;IAED,IAAW,aAAa,gCAEvB;IAED,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,4BAA4B;IAIrE,MAAM;IAIN,cAAc,CAAC,IAAI,SAAK;IAIxB,OAAO;;;IAIP,iBAAiB;IAIjB,iBAAiB,CAAC,GAAG,EAAE,MAAM;IAI7B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,4BAA4B;IAoBxD,IAAI,EAAE,WAEL;IAED,OAAO;IAKP;;;;OAIG;IACU,eAAe;IAS5B,2EAA2E;IACpE,eAAe;IAIf,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,4BAA4B,EAAE,GAAG;;KAAiB;IA8BrF;;;;OAIG;IACI,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,4BAA4B;IAoDvE,uEAAuE;IACvE,OAAO,CAAC,oBAAoB;IAQ5B,0EAA0E;IAC1E,OAAO,CAAC,iBAAiB;CAkB1B"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { WsServerSocket } from '@cuboapp/ws';
|
|
2
|
+
import { CuboCrdtMutation, CuboCrdtRowId } from '../types';
|
|
3
|
+
import { CuboCrdtClientDocOrigin } from '../client';
|
|
4
|
+
import { CuboCrdtServerDocument } from './document';
|
|
5
|
+
import { CuboCrdtServerOptions } from './types';
|
|
6
|
+
export * from './document';
|
|
7
|
+
export * from './types';
|
|
8
|
+
export declare class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extract<keyof M, string>> {
|
|
9
|
+
private options;
|
|
10
|
+
constructor(options: CuboCrdtServerOptions<M, A, E>);
|
|
11
|
+
private clients;
|
|
12
|
+
private queue;
|
|
13
|
+
private sendTimeouts;
|
|
14
|
+
private subscribes;
|
|
15
|
+
private subscribesByClient;
|
|
16
|
+
private subscribesByEntity;
|
|
17
|
+
private subscribeRefreshStates;
|
|
18
|
+
private subscribeGenerations;
|
|
19
|
+
private documents;
|
|
20
|
+
private documentEvictions;
|
|
21
|
+
private subscribesByDocument;
|
|
22
|
+
private documentsBySubscribe;
|
|
23
|
+
init(): Promise<void>;
|
|
24
|
+
destroy(): Promise<void>;
|
|
25
|
+
addClient(client: WsServerSocket<{
|
|
26
|
+
auth?: A;
|
|
27
|
+
}>): void;
|
|
28
|
+
removeClient(client: WsServerSocket): void;
|
|
29
|
+
private get batchMaxSize();
|
|
30
|
+
private get batchDebounce();
|
|
31
|
+
private get paginationDebounce();
|
|
32
|
+
private resolvePaginated;
|
|
33
|
+
private sendQueueToClient;
|
|
34
|
+
sendToClient(client_id: string, data: any): void;
|
|
35
|
+
private linkSubscribeToDocument;
|
|
36
|
+
private unlinkSubscribeFromDocument;
|
|
37
|
+
private checkDocumentNeedRemove;
|
|
38
|
+
private cleanSubscribe;
|
|
39
|
+
getDocument(entity: string, entity_id: CuboCrdtRowId): CuboCrdtServerDocument;
|
|
40
|
+
getSutableSubscribes(entity: string, row: any): string[];
|
|
41
|
+
private getSubscribeRefreshState;
|
|
42
|
+
private bumpSubscribeGeneration;
|
|
43
|
+
private runSubscribeRefresh;
|
|
44
|
+
private requestSubscribeRefresh;
|
|
45
|
+
private getSubscribeQueryKeys;
|
|
46
|
+
private shouldRefreshSubscribe;
|
|
47
|
+
/**
|
|
48
|
+
* Уведомляет пагинированные подписки о сохранённой мутации.
|
|
49
|
+
* Пересчитываются только реально открытые страницы; несколько мутаций
|
|
50
|
+
* схлопываются debounce-ом в один запрос на подписку.
|
|
51
|
+
*/
|
|
52
|
+
notifyMutation(entity: E, mutation: CuboCrdtMutation<M[E]>): void;
|
|
53
|
+
/**
|
|
54
|
+
* Принудительно инвалидирует все открытые пагинированные окна сущности.
|
|
55
|
+
* Используется для вычисляемых полей и зависимых сущностей, когда changedKeys
|
|
56
|
+
* исходной строки недостаточно для определения влияния на запрос.
|
|
57
|
+
*/
|
|
58
|
+
invalidate(entity: E): void;
|
|
59
|
+
private getOrCreateDocument;
|
|
60
|
+
ensureDocument(entity: E, row: any, awareness?: boolean): CuboCrdtServerDocument;
|
|
61
|
+
private sendDocumentToSubscribe;
|
|
62
|
+
deleteDocument(entity: E, row: any, origin?: CuboCrdtClientDocOrigin): void;
|
|
63
|
+
private onDocumentExternalCreate;
|
|
64
|
+
private onDocumentExternalUpdate;
|
|
65
|
+
private onAwarenessExternalUpdate;
|
|
66
|
+
private onDocumentExternalDelete;
|
|
67
|
+
private checkSubscribeIsSutable;
|
|
68
|
+
private checkRowIsSutable;
|
|
69
|
+
private pushDocumentAction;
|
|
70
|
+
private reconcileSubscribe;
|
|
71
|
+
private upgradeSubscribe;
|
|
72
|
+
private initSubscribe;
|
|
73
|
+
private get debug();
|
|
74
|
+
private get ws();
|
|
75
|
+
private get entities();
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { CuboCrdtAction, CuboCrdtExposeStrategy } from '../../types';
|
|
2
|
+
export type CuboCrdtServerDocumentStoreContext = {
|
|
3
|
+
previousRow: object;
|
|
4
|
+
row: object;
|
|
5
|
+
};
|
|
6
|
+
export type CuboCrdtServerDocumentStoreWaiter = {
|
|
7
|
+
resolve: () => void;
|
|
8
|
+
reject: (reason?: unknown) => void;
|
|
9
|
+
};
|
|
10
|
+
export type CuboCrdtServerDocumentPendingStore = {
|
|
11
|
+
body: object;
|
|
12
|
+
changeKey: string;
|
|
13
|
+
origin: CuboCrdtServerDocumentOrigin;
|
|
14
|
+
originKey: string;
|
|
15
|
+
row: object;
|
|
16
|
+
waiters: CuboCrdtServerDocumentStoreWaiter[];
|
|
17
|
+
};
|
|
18
|
+
export type CuboCrdtServerDocumentOptions = {
|
|
19
|
+
name: string;
|
|
20
|
+
onStore?: (body: object, origin: CuboCrdtServerDocumentOrigin, context: CuboCrdtServerDocumentStoreContext) => void | Promise<void>;
|
|
21
|
+
onUpdate?: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => void;
|
|
22
|
+
onAwarenessUpdate?: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => void;
|
|
23
|
+
initialState?: object;
|
|
24
|
+
awareness?: boolean;
|
|
25
|
+
yjsOptions?: {
|
|
26
|
+
guid?: string;
|
|
27
|
+
collectionid?: string;
|
|
28
|
+
gc?: boolean;
|
|
29
|
+
gcFilter?: () => true;
|
|
30
|
+
meta?: any;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
export type CuboCrdtServerDocumentIncomingAction = {
|
|
34
|
+
action: CuboCrdtAction;
|
|
35
|
+
entity: string;
|
|
36
|
+
entity_id: number;
|
|
37
|
+
data: number[];
|
|
38
|
+
origin: CuboCrdtServerDocumentOrigin;
|
|
39
|
+
};
|
|
40
|
+
export type CuboCrdtServerDocumentOrigin = {
|
|
41
|
+
store?: boolean;
|
|
42
|
+
react?: boolean;
|
|
43
|
+
keys?: string[];
|
|
44
|
+
expose?: CuboCrdtExposeStrategy;
|
|
45
|
+
subscribe_id?: string;
|
|
46
|
+
client_id?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Снимок серверного контекста автора в момент получения update.
|
|
49
|
+
*
|
|
50
|
+
* Он нужен только для отложенного сохранения: исходная подписка и сокет могут
|
|
51
|
+
* закрыться при refresh раньше, чем сработает debounce. Клиентское значение
|
|
52
|
+
* этого поля сервер всегда перезаписывает собственными данными.
|
|
53
|
+
*/
|
|
54
|
+
store_context?: {
|
|
55
|
+
client_id: string;
|
|
56
|
+
auth?: unknown;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../../../src/server/types/document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAEpE,MAAM,MAAM,kCAAkC,GAAG;IAC/C,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,iCAAiC,GAAG;IAC9C,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,kCAAkC,GAAG;IAC/C,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,4BAA4B,CAAA;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,iCAAiC,EAAE,CAAA;CAC7C,CAAA;AAED,MAAM,MAAM,6BAA6B,GAAG;IAC1C,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,CACR,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,4BAA4B,EACpC,OAAO,EAAE,kCAAkC,KACxC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEzB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,4BAA4B,KAAK,IAAI,CAAA;IAC5E,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,4BAA4B,KAAK,IAAI,CAAA;IAGrF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,UAAU,CAAC,EAAE;QACX,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,EAAE,CAAC,EAAE,OAAO,CAAA;QACZ,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;QACrB,IAAI,CAAC,EAAE,GAAG,CAAA;KACX,CAAA;CACF,CAAA;AAED,MAAM,MAAM,oCAAoC,GAAG;IAEjD,MAAM,EAAE,cAAc,CAAA;IAGtB,MAAM,EAAE,MAAM,CAAA;IAGd,SAAS,EAAE,MAAM,CAAA;IAGjB,IAAI,EAAE,MAAM,EAAE,CAAA;IAGd,MAAM,EAAE,4BAA4B,CAAA;CACrC,CAAA;AAED,MAAM,MAAM,4BAA4B,GAAG;IAEzC,KAAK,CAAC,EAAE,OAAO,CAAA;IAGf,KAAK,CAAC,EAAE,OAAO,CAAA;IAGf,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IAGf,MAAM,CAAC,EAAE,sBAAsB,CAAA;IAG/B,YAAY,CAAC,EAAE,MAAM,CAAA;IAGrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE;QACd,SAAS,EAAE,MAAM,CAAA;QACjB,IAAI,CAAC,EAAE,OAAO,CAAA;KACf,CAAA;CACF,CAAA"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { WsServer, WsServerSocket } from '@cuboapp/ws';
|
|
2
|
+
import { CuboCrdtListTotals, CuboCrdtMutation } from '../../types';
|
|
3
|
+
import { CuboCrdtServerDocument } from '../document';
|
|
4
|
+
import { CuboCrdtServerDocumentOrigin } from './document';
|
|
5
|
+
import { CuboCrdtServerSubscribe } from './subscribe';
|
|
6
|
+
export * from './document';
|
|
7
|
+
export * from './subscribe';
|
|
8
|
+
export type CuboCrdtServerFetchResult<T> = {
|
|
9
|
+
rows: T[];
|
|
10
|
+
totals?: CuboCrdtListTotals;
|
|
11
|
+
};
|
|
12
|
+
export type CuboCrdtServerOptions<M, A, E extends Extract<keyof M, string>> = {
|
|
13
|
+
ws: WsServer;
|
|
14
|
+
entities: E[];
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Параметры батчинга исходящих событий (защита от перегрузки бэка).
|
|
18
|
+
* - maxSize: при достижении такого размера очереди клиента батч отправляется сразу
|
|
19
|
+
* - debounce: задержка перед отправкой неполного батча (мс)
|
|
20
|
+
*/
|
|
21
|
+
batch?: {
|
|
22
|
+
maxSize?: number;
|
|
23
|
+
debounce?: number;
|
|
24
|
+
};
|
|
25
|
+
pagination?: {
|
|
26
|
+
debounce?: number;
|
|
27
|
+
};
|
|
28
|
+
fetchRows?: (client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) => Promise<M[E][] | CuboCrdtServerFetchResult<M[E]>>;
|
|
29
|
+
checkRowIsSutable?: (baseState: boolean, ctx: {
|
|
30
|
+
entity: E;
|
|
31
|
+
subscribe: CuboCrdtServerSubscribe;
|
|
32
|
+
row: any;
|
|
33
|
+
}) => boolean;
|
|
34
|
+
shouldRefreshSubscribe?: (ctx: {
|
|
35
|
+
entity: E;
|
|
36
|
+
subscribe: CuboCrdtServerSubscribe;
|
|
37
|
+
mutation: CuboCrdtMutation<M[E]>;
|
|
38
|
+
}) => boolean;
|
|
39
|
+
storeRow?: <K extends E>(entity: K, entity_id: number, row: M[K], opts: {
|
|
40
|
+
document: CuboCrdtServerDocument;
|
|
41
|
+
client: CuboCrdtSocketClient<A>;
|
|
42
|
+
origin: CuboCrdtServerDocumentOrigin;
|
|
43
|
+
previousRow: M[K];
|
|
44
|
+
row: M[K];
|
|
45
|
+
}) => Promise<void> | void;
|
|
46
|
+
};
|
|
47
|
+
export type CuboCrdtSocketClient<A> = WsServerSocket<{
|
|
48
|
+
auth?: A;
|
|
49
|
+
}>;
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAClE,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAA;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAErD,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAE3B,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI;IACzC,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,MAAM,CAAC,EAAE,kBAAkB,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,qBAAqB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI;IAC5E,EAAE,EAAE,QAAQ,CAAA;IACZ,QAAQ,EAAE,CAAC,EAAE,CAAA;IACb,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;OAIG;IACH,KAAK,CAAC,EAAE;QACN,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;KAClB,CAAA;IACD,UAAU,CAAC,EAAE;QACX,QAAQ,CAAC,EAAE,MAAM,CAAA;KAClB,CAAA;IACD,SAAS,CAAC,EAAE,CACV,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,uBAAuB,KAC/B,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,yBAAyB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACtD,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE;QAAE,MAAM,EAAE,CAAC,CAAC;QAAC,SAAS,EAAE,uBAAuB,CAAC;QAAC,GAAG,EAAE,GAAG,CAAA;KAAE,KAAK,OAAO,CAAA;IACrH,sBAAsB,CAAC,EAAE,CAAC,GAAG,EAAE;QAC7B,MAAM,EAAE,CAAC,CAAA;QACT,SAAS,EAAE,uBAAuB,CAAA;QAClC,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;KACjC,KAAK,OAAO,CAAA;IACb,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,EACrB,MAAM,EAAE,CAAC,EACT,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,EACT,IAAI,EAAE;QACJ,QAAQ,EAAE,sBAAsB,CAAA;QAChC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC,CAAA;QAC/B,MAAM,EAAE,4BAA4B,CAAA;QACpC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;QACjB,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;KACV,KACE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,oBAAoB,CAAC,CAAC,IAAI,cAAc,CAAC;IAAE,IAAI,CAAC,EAAE,CAAC,CAAA;CAAE,CAAC,CAAA"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CuboCrdtListTotals, CuboCrdtRowId } from '../../types';
|
|
2
|
+
export type CuboCrdtServerUnsubscribeDto = {
|
|
3
|
+
subscribe_id: string;
|
|
4
|
+
};
|
|
5
|
+
export type CuboCrdtServerSubscribeDto = {
|
|
6
|
+
subscribe_id: string;
|
|
7
|
+
entity: string;
|
|
8
|
+
filters: {
|
|
9
|
+
[K in 'id' | string]: any;
|
|
10
|
+
};
|
|
11
|
+
awareness?: boolean;
|
|
12
|
+
paginated?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Подписка только на КОЛИЧЕСТВО строк под фильтрами.
|
|
15
|
+
*
|
|
16
|
+
* Документы при этом не создаются вовсе: клиенту нужно одно число (счётчик,
|
|
17
|
+
* индикатор непрочитанного), а держать ради него весь набор строк — держать
|
|
18
|
+
* лишнюю память на сервере и лишний трафик на каждое их изменение.
|
|
19
|
+
*/
|
|
20
|
+
counted?: boolean;
|
|
21
|
+
};
|
|
22
|
+
export type CuboCrdtServerSubscribe = {
|
|
23
|
+
id: string;
|
|
24
|
+
client_id: string;
|
|
25
|
+
row_ids: CuboCrdtRowId[];
|
|
26
|
+
totals: CuboCrdtListTotals;
|
|
27
|
+
revision: number;
|
|
28
|
+
} & Pick<CuboCrdtServerSubscribeDto, 'entity' | 'filters' | 'awareness' | 'paginated' | 'counted'>;
|
|
29
|
+
export type CuboCrdtSubscribeRefreshState = {
|
|
30
|
+
dirty: boolean;
|
|
31
|
+
forceSync: boolean;
|
|
32
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
33
|
+
running?: Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=subscribe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../../src/server/types/subscribe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEpE,MAAM,MAAM,4BAA4B,GAAG;IACzC,YAAY,EAAE,MAAM,CAAA;CACrB,CAAA;AACD,MAAM,MAAM,0BAA0B,GAAG;IACvC,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE;SACN,CAAC,IAAI,IAAI,GAAG,MAAM,GAAG,GAAG;KAC1B,CAAA;IACD,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,aAAa,EAAE,CAAA;IACxB,MAAM,EAAE,kBAAkB,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;CACjB,GAAG,IAAI,CAAC,0BAA0B,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,SAAS,CAAC,CAAA;AAElG,MAAM,MAAM,6BAA6B,GAAG;IAC1C,KAAK,EAAE,OAAO,CAAA;IACd,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAA;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CACxB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/utils/index.ts"],"names":[],"mappings":"AAiCA,2EAA2E;AAC3E,eAAO,MAAM,qBAAqB,GAAI,UAAU,MAAM,EAAE,SAAS,MAAM,WAMtE,CAAA"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type CuboCrdtKey<K, M> = K extends Extract<keyof M, string> ? M[K] : any;
|
|
2
|
+
export type CuboCrdtAction = 'create' | 'upsert' | 'update' | 'delete' | 'awareness' | 'sync';
|
|
3
|
+
export type CuboCrdtExposeStrategy = 'all' | 'other' | 'subscribe' | 'client';
|
|
4
|
+
export type CuboCrdtListTotals = Record<string, number>;
|
|
5
|
+
export type CuboCrdtSubscriptionStatus = 'idle' | 'loading' | 'ready' | 'error';
|
|
6
|
+
/**
|
|
7
|
+
* A row's primary key, as it comes off the wire.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately NOT `number`. Rows are keyed by whatever the database column holds — uuid text in
|
|
10
|
+
* current schemas, integers in older ones — and the CRDT plane only ever compares and indexes ids, it
|
|
11
|
+
* never does arithmetic on them. Typing this as `number` is what let `Number(id)` / `Number.isFinite`
|
|
12
|
+
* creep into both ends of the sync, where every uuid failed the test and the row was silently dropped.
|
|
13
|
+
*/
|
|
14
|
+
export type CuboCrdtRowId = string | number;
|
|
15
|
+
export type CuboCrdtListSyncData = {
|
|
16
|
+
ids: CuboCrdtRowId[];
|
|
17
|
+
totals: CuboCrdtListTotals;
|
|
18
|
+
revision: number;
|
|
19
|
+
};
|
|
20
|
+
export type CuboCrdtMutationAction = 'create' | 'update' | 'delete';
|
|
21
|
+
export type CuboCrdtMutation<T = any> = {
|
|
22
|
+
action: CuboCrdtMutationAction;
|
|
23
|
+
row: T;
|
|
24
|
+
previousRow?: T;
|
|
25
|
+
changedKeys?: string[];
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,SAAS,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAA;AAE/E,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,MAAM,CAAA;AAE7F,MAAM,MAAM,sBAAsB,GAAG,KAAK,GAAG,OAAO,GAAG,WAAW,GAAG,QAAQ,CAAA;AAE7E,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAEvD,MAAM,MAAM,0BAA0B,GAClC,MAAM,GACN,SAAS,GACT,OAAO,GACP,OAAO,CAAA;AAEX;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAA;AAE3C,MAAM,MAAM,oBAAoB,GAAG;IACjC,GAAG,EAAE,aAAa,EAAE,CAAA;IACpB,MAAM,EAAE,kBAAkB,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAEnE,MAAM,MAAM,gBAAgB,CAAC,CAAC,GAAG,GAAG,IAAI;IACtC,MAAM,EAAE,sBAAsB,CAAA;IAC9B,GAAG,EAAE,CAAC,CAAA;IACN,WAAW,CAAC,EAAE,CAAC,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB,CAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { CuboApiEntitiesMap } from '@cuboapp/types';
|
|
2
|
+
import { CuboCrdtClient, CuboCrdtClientOptions, CuboCrdtRowId, CuboCrdtServer, CuboCrdtServerDocumentOrigin, CuboCrdtServerOptions, CuboCrdtServerSubscribe } from '..';
|
|
3
|
+
export declare function createCrdtServer<M extends CuboApiEntitiesMap<M>, A = {}, E extends Extract<keyof M, string> = Extract<keyof M, string>>(opts: CuboCrdtServerOptions<M, A, E>): CuboCrdtServer<M, A, E>;
|
|
4
|
+
export declare function createCrdtClient<M extends CuboApiEntitiesMap<M>>(opts: CuboCrdtClientOptions<M>): CuboCrdtClient<M>;
|
|
5
|
+
export declare function cleanObject<F>(object: F): any;
|
|
6
|
+
export declare function checkRowIsSutable(row: any, filters?: object, customKeys?: Record<string, (key: string, row: any, filters?: any) => boolean>): boolean;
|
|
7
|
+
export declare function checkSubscribeIsSutable(origin: CuboCrdtServerDocumentOrigin, subscribe?: CuboCrdtServerSubscribe): boolean;
|
|
8
|
+
export declare function areCrdtListIdsEqual(left: CuboCrdtRowId[], right: CuboCrdtRowId[]): boolean;
|
|
9
|
+
export declare function areCrdtListTotalsEqual(left: Record<string, number>, right: Record<string, number>): boolean;
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,aAAa,EACb,cAAc,EACd,4BAA4B,EAC5B,qBAAqB,EACrB,uBAAuB,EACxB,MAAM,IAAI,CAAA;AAEX,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EACrI,IAAI,EAAE,qBAAqB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,2BAGrC;AAED,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,kBAAkB,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,qBAAqB,CAAC,CAAC,CAAC,qBAE/F;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,GAAG,CAkB7C;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,GAAG,EACR,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,WAwD/E;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,4BAA4B,EAAE,SAAS,CAAC,EAAE,uBAAuB,WAWhH;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,WAIhF;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,WAI9B"}
|
package/package.json
CHANGED
|
@@ -1,35 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cuboapp/crdt",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "CRDT
|
|
5
|
-
"main": "src/index.ts",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git@cuboapp.gitlab.yandexcloud.net:cubo/crdt.git"
|
|
9
|
-
},
|
|
10
|
-
"author": "CuboSoft",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Yjs-based CRDT server/client over @cuboapp/ws (vendored — README §5/§13.1).",
|
|
11
5
|
"license": "MIT",
|
|
12
6
|
"type": "module",
|
|
13
|
-
"
|
|
14
|
-
|
|
7
|
+
"main": "src/index.ts",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./src/index.ts"
|
|
13
|
+
}
|
|
15
14
|
},
|
|
16
15
|
"files": [
|
|
16
|
+
"dist",
|
|
17
17
|
"src"
|
|
18
18
|
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"typecheck": "echo \"vendored @cuboapp — typecheck via emitted d.ts\"",
|
|
22
|
+
"test": "echo \"no tests\"",
|
|
23
|
+
"lint": "echo \"no lint\""
|
|
24
|
+
},
|
|
19
25
|
"dependencies": {
|
|
20
|
-
"@cuboapp/types": "^
|
|
21
|
-
"@cuboapp/utils": "1.0.12",
|
|
22
|
-
"@cuboapp/ws": "
|
|
26
|
+
"@cuboapp/types": "^3.0.0",
|
|
27
|
+
"@cuboapp/utils": "^1.0.12",
|
|
28
|
+
"@cuboapp/ws": "^2.0.0",
|
|
23
29
|
"y-protocols": "^1.0.7",
|
|
24
30
|
"yjs": "^13.6.31"
|
|
25
31
|
},
|
|
26
32
|
"peerDependencies": {
|
|
27
|
-
"vue": "^3.5.
|
|
28
|
-
"vue-router": "^5.1.0"
|
|
33
|
+
"vue": "^3.5.0"
|
|
29
34
|
},
|
|
30
35
|
"devDependencies": {
|
|
31
|
-
"@types/node": "^
|
|
32
|
-
"
|
|
33
|
-
"
|
|
36
|
+
"@types/node": "^22.10.5",
|
|
37
|
+
"typescript": "^5.8.3",
|
|
38
|
+
"vue": "^3.5.13"
|
|
34
39
|
}
|
|
35
40
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { keyBy, pick, uuid } from '@cuboapp/utils'
|
|
2
2
|
import { WsClientEvent } from '@cuboapp/ws'
|
|
3
|
-
import { computed, reactive } from 'vue'
|
|
3
|
+
import { computed, reactive, shallowReactive } from 'vue'
|
|
4
4
|
import { applyAwarenessUpdate, Awareness, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness'
|
|
5
5
|
import { applyUpdate, Doc } from 'yjs'
|
|
6
6
|
|
|
7
7
|
import { CUBO_CRDT_EVENT } from '../constants'
|
|
8
8
|
import { CuboCrdtServerDocumentIncomingAction } from '../server'
|
|
9
|
-
import { CuboCrdtKey, CuboCrdtListSyncData, CuboCrdtSubscriptionStatus } from '../types'
|
|
9
|
+
import { CuboCrdtKey, CuboCrdtListSyncData, CuboCrdtRowId, CuboCrdtSubscriptionStatus } from '../types'
|
|
10
10
|
|
|
11
11
|
import { AsyncSerialQueue } from './queue'
|
|
12
12
|
import {
|
|
@@ -27,7 +27,7 @@ export class CuboCrdtClient<M> {
|
|
|
27
27
|
public store: CuboCrdtClientStore<M> = {}
|
|
28
28
|
// Реактивен только реестр: Vue отслеживает set/delete и повторно вычисляет
|
|
29
29
|
// computed(useDoc). Сами Y.Doc/Awareness остаются исходными объектами без proxy.
|
|
30
|
-
public docs = new Map<string, CuboCrdtClientDoc>()
|
|
30
|
+
public docs = shallowReactive(new Map<string, CuboCrdtClientDoc>())
|
|
31
31
|
|
|
32
32
|
private listeners: Map<string, Map<string, (ctx: CuboCrdtClientSubscribeEvent) => void | Promise<void>>> = new Map()
|
|
33
33
|
|
|
@@ -210,9 +210,11 @@ export class CuboCrdtClient<M> {
|
|
|
210
210
|
return computed(() => (this.store[entity]?.state.rows || []) as M[K][])
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
public useComputedRow<K extends Extract<keyof M, string>>(entity: K, entity_id:
|
|
213
|
+
public useComputedRow<K extends Extract<keyof M, string>>(entity: K, entity_id: CuboCrdtRowId) {
|
|
214
214
|
return computed(() => {
|
|
215
|
-
|
|
215
|
+
// Compared as strings for the same reason the sync path is: the caller may hold a uuid while the
|
|
216
|
+
// row carries one too, but a legacy numeric id can arrive as either, and `'42' === 42` is false.
|
|
217
|
+
return ((this.store[entity]?.state.rows || []) as M[K][]).find((row: any) => String(row.id) === String(entity_id))
|
|
216
218
|
})
|
|
217
219
|
}
|
|
218
220
|
|
|
@@ -351,7 +353,7 @@ export class CuboCrdtClient<M> {
|
|
|
351
353
|
|
|
352
354
|
const item = this.store[storeKey]
|
|
353
355
|
item?.state.rows.forEach((row) => {
|
|
354
|
-
this.detachDocument(`${entity}`, (row as any).id, subscribe_id
|
|
356
|
+
this.detachDocument(`${entity}`, (row as any).id, subscribe_id)
|
|
355
357
|
})
|
|
356
358
|
|
|
357
359
|
// очищаем стор
|
|
@@ -427,7 +429,7 @@ export class CuboCrdtClient<M> {
|
|
|
427
429
|
|
|
428
430
|
public update<K extends Extract<keyof M, string>>(
|
|
429
431
|
entity: K,
|
|
430
|
-
entity_id:
|
|
432
|
+
entity_id: CuboCrdtRowId,
|
|
431
433
|
dto: K extends Extract<keyof M, string> ? Partial<M[K]> : object,
|
|
432
434
|
opts?: CuboCrdtClientDocOrigin
|
|
433
435
|
) {
|
|
@@ -496,7 +498,7 @@ export class CuboCrdtClient<M> {
|
|
|
496
498
|
})
|
|
497
499
|
}
|
|
498
500
|
|
|
499
|
-
private applyDocumentRows(entity_id:
|
|
501
|
+
private applyDocumentRows(entity_id: CuboCrdtRowId, document: CuboCrdtClientDoc) {
|
|
500
502
|
for (const subscribe_id of document.subscribes) {
|
|
501
503
|
const subscription = this.subscriptions.get(subscribe_id)
|
|
502
504
|
if (subscription) {
|
|
@@ -505,7 +507,7 @@ export class CuboCrdtClient<M> {
|
|
|
505
507
|
}
|
|
506
508
|
}
|
|
507
509
|
|
|
508
|
-
private attachDocument(entity: string, entity_id:
|
|
510
|
+
private attachDocument(entity: string, entity_id: CuboCrdtRowId, document: CuboCrdtClientDoc, opts: CuboCrdtClientDocUpdateOptions) {
|
|
509
511
|
document.subscribes.add(opts.subscribe_id)
|
|
510
512
|
|
|
511
513
|
if (!opts.awareness) {
|
|
@@ -547,7 +549,7 @@ export class CuboCrdtClient<M> {
|
|
|
547
549
|
}
|
|
548
550
|
}
|
|
549
551
|
|
|
550
|
-
private detachDocument(entity: string, entity_id:
|
|
552
|
+
private detachDocument(entity: string, entity_id: CuboCrdtRowId, subscribe_id: string) {
|
|
551
553
|
const key = `${entity}:${entity_id}`
|
|
552
554
|
const document = this.docs.get(key)
|
|
553
555
|
if (!document) {
|
|
@@ -571,7 +573,7 @@ export class CuboCrdtClient<M> {
|
|
|
571
573
|
}
|
|
572
574
|
|
|
573
575
|
// апсертит строку в реактивное хранилище из текущего состояния yjs-документа
|
|
574
|
-
private applyRowJson(storeKey: string, entity_id:
|
|
576
|
+
private applyRowJson(storeKey: string, entity_id: CuboCrdtRowId, doc: Doc) {
|
|
575
577
|
const json: any = doc.getMap().toJSON()
|
|
576
578
|
const rows = this.store[storeKey]?.state.rows
|
|
577
579
|
if (!rows) {
|
|
@@ -620,11 +622,11 @@ export class CuboCrdtClient<M> {
|
|
|
620
622
|
|
|
621
623
|
applyUpdate(yjsDoc, update, { react: false })
|
|
622
624
|
|
|
623
|
-
const document
|
|
625
|
+
const document = shallowReactive<CuboCrdtClientDoc>({
|
|
624
626
|
doc: yjsDoc,
|
|
625
627
|
subscribes: new Set(),
|
|
626
628
|
awarenessSubscribes: new Set()
|
|
627
|
-
}
|
|
629
|
+
})
|
|
628
630
|
this.attachDocument(ctx.entity, entity_id, document, opts)
|
|
629
631
|
// Публикуем контейнер реактивному реестру только после полной инициализации.
|
|
630
632
|
// Тогда первый computed(useDoc) не увидит промежуточный doc без Awareness.
|
|
@@ -775,21 +777,29 @@ export class CuboCrdtClient<M> {
|
|
|
775
777
|
return
|
|
776
778
|
}
|
|
777
779
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
+
// An id is OPAQUE. This used to be `filter(Number.isFinite)`, from when rows had integer ids —
|
|
781
|
+
// against uuids every id failed the test, `ids` came back empty, and the splice below then wiped
|
|
782
|
+
// the whole list. Silently: `totals` is copied from the payload separately, so the subscription
|
|
783
|
+
// reported the true count while holding no rows, which reads exactly like "the account has none".
|
|
784
|
+
//
|
|
785
|
+
// Compared as STRINGS throughout: the wire carries whatever the database column holds — uuid text
|
|
786
|
+
// today, integers in older data — and `'42' !== 42` would resurrect the same bug for anyone still
|
|
787
|
+
// on numeric ids.
|
|
788
|
+
const ids = data.ids.filter((id) => id !== null && id !== undefined && id !== '')
|
|
789
|
+
const idsSet = new Set(ids.map((id) => String(id)))
|
|
780
790
|
|
|
781
791
|
for (const row of item.state.rows) {
|
|
782
792
|
const entity_id = (row as any).id
|
|
783
793
|
|
|
784
|
-
if (idsSet.has(entity_id)) {
|
|
794
|
+
if (idsSet.has(String(entity_id))) {
|
|
785
795
|
continue
|
|
786
796
|
}
|
|
787
797
|
|
|
788
|
-
this.detachDocument(ctx.entity, entity_id, opts.subscribe_id
|
|
798
|
+
this.detachDocument(ctx.entity, entity_id, opts.subscribe_id)
|
|
789
799
|
}
|
|
790
800
|
|
|
791
|
-
const rowsById = new Map(item.state.rows.map((row: any) => [
|
|
792
|
-
const orderedRows = ids.map((id) => rowsById.get(id)).filter((row) => row !== undefined)
|
|
801
|
+
const rowsById = new Map(item.state.rows.map((row: any) => [String(row.id), row]))
|
|
802
|
+
const orderedRows = ids.map((id) => rowsById.get(String(id))).filter((row) => row !== undefined)
|
|
793
803
|
|
|
794
804
|
item.state.rows.splice(0, item.state.rows.length, ...orderedRows)
|
|
795
805
|
item.state.totals = data.totals || {}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type WsClient } from '@cuboapp/ws'
|
|
2
2
|
import { ComputedRef } from 'vue'
|
|
3
|
-
import { Awareness } from 'y-protocols/awareness'
|
|
4
3
|
|
|
5
4
|
import { CuboCrdtAction, CuboCrdtListSyncData, CuboCrdtListTotals, CuboCrdtSubscriptionStatus } from '../../types'
|
|
6
5
|
|
|
@@ -39,7 +38,6 @@ export type CuboCrdtClientSubscribeEvent = {
|
|
|
39
38
|
export type CuboCrdtClientList<T> = {
|
|
40
39
|
subscribe_id: string
|
|
41
40
|
storeKey: string
|
|
42
|
-
awarenesses?: ComputedRef<Map<number, Awareness> | undefined>
|
|
43
41
|
subscribe: (filters?: any) => void
|
|
44
42
|
upgrade: (filters?: any) => void
|
|
45
43
|
unsubscribe: (clear?: boolean) => void
|
package/src/server/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { WsServerSocket } from '@cuboapp/ws'
|
|
|
3
3
|
import { applyAwarenessUpdate, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness'
|
|
4
4
|
|
|
5
5
|
import { CUBO_CRDT_EVENT } from '../constants'
|
|
6
|
-
import { CuboCrdtAction, CuboCrdtListSyncData, CuboCrdtMutation } from '../types'
|
|
6
|
+
import { CuboCrdtAction, CuboCrdtListSyncData, CuboCrdtMutation, CuboCrdtRowId } from '../types'
|
|
7
7
|
import { areCrdtListIdsEqual, areCrdtListTotalsEqual, checkRowIsSutable } from '../utils'
|
|
8
8
|
|
|
9
9
|
import { CuboCrdtClientDocOrigin } from '../client'
|
|
@@ -473,7 +473,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
473
473
|
this.subscribesByEntity.get(subscribe.entity as E)?.delete(subscribe_id)
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
-
public getDocument(entity: string, entity_id:
|
|
476
|
+
public getDocument(entity: string, entity_id: CuboCrdtRowId) {
|
|
477
477
|
return this.documents.get(`${entity}:${entity_id}`)
|
|
478
478
|
}
|
|
479
479
|
|
|
@@ -767,7 +767,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
767
767
|
// Точечная доставка документа ТОЛЬКО инициирующей подписке (init/upgrade/resubscribe).
|
|
768
768
|
// Раньше это шло через ensureDocument -> pushDocumentAction('upsert') с полным сканом
|
|
769
769
|
// всех подписок сущности — на каждый ряд из fetchRows. Теперь — один send на подписку.
|
|
770
|
-
private sendDocumentToSubscribe(subscribe: CuboCrdtServerSubscribe, document: CuboCrdtServerDocument, entity_id:
|
|
770
|
+
private sendDocumentToSubscribe(subscribe: CuboCrdtServerSubscribe, document: CuboCrdtServerDocument, entity_id: CuboCrdtRowId) {
|
|
771
771
|
this.linkSubscribeToDocument(subscribe.id, document.name)
|
|
772
772
|
|
|
773
773
|
this.sendToClient(subscribe.client_id, {
|
|
@@ -1054,24 +1054,35 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
1054
1054
|
return true
|
|
1055
1055
|
}
|
|
1056
1056
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1057
|
+
// Take the row's id AS IT IS. This was `Number(row.id)` guarded by `Number.isFinite`, which against
|
|
1058
|
+
// uuid keys evaluated NaN for every row and `continue`d past all of them: `nextIds` came out empty,
|
|
1059
|
+
// no document was ever created or linked, and the subscription synced `ids: []` while `totals` —
|
|
1060
|
+
// assigned separately from the fetch — still reported the true count. So the client was told "19
|
|
1061
|
+
// rows exist" and handed none of them, with nothing logged at either end.
|
|
1062
|
+
//
|
|
1063
|
+
// Keyed by String so a uuid and a legacy integer id both index correctly, and so `'42'` and `42`
|
|
1064
|
+
// cannot become two entries for one row.
|
|
1065
|
+
const rowsById = new Map<string, any>()
|
|
1066
|
+
const nextIds: CuboCrdtRowId[] = []
|
|
1059
1067
|
|
|
1060
1068
|
for (const row of result.rows) {
|
|
1061
|
-
const id =
|
|
1062
|
-
if (
|
|
1069
|
+
const id = (row as any)?.id
|
|
1070
|
+
if (id === null || id === undefined || id === '' || rowsById.has(String(id))) {
|
|
1063
1071
|
continue
|
|
1064
1072
|
}
|
|
1065
1073
|
|
|
1066
|
-
rowsById.set(id, row)
|
|
1074
|
+
rowsById.set(String(id), row)
|
|
1067
1075
|
nextIds.push(id)
|
|
1068
1076
|
}
|
|
1069
1077
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1078
|
+
// Membership is compared as STRINGS, like the map above: `row_ids` carries whatever was stored on
|
|
1079
|
+
// the previous pass, so a set of raw values would treat `'42'` and `42` as two different rows and
|
|
1080
|
+
// churn documents on every reconcile.
|
|
1081
|
+
const previousIds = new Set(subscribe.row_ids.map((id) => String(id)))
|
|
1082
|
+
const nextIdsSet = new Set(nextIds.map((id) => String(id)))
|
|
1072
1083
|
|
|
1073
1084
|
for (const entity_id of subscribe.row_ids) {
|
|
1074
|
-
if (nextIdsSet.has(entity_id)) {
|
|
1085
|
+
if (nextIdsSet.has(String(entity_id))) {
|
|
1075
1086
|
continue
|
|
1076
1087
|
}
|
|
1077
1088
|
|
|
@@ -1081,12 +1092,12 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
1081
1092
|
}
|
|
1082
1093
|
|
|
1083
1094
|
for (const entity_id of nextIds) {
|
|
1084
|
-
const row = rowsById.get(entity_id)
|
|
1095
|
+
const row = rowsById.get(String(entity_id))
|
|
1085
1096
|
if (!row) {
|
|
1086
1097
|
continue
|
|
1087
1098
|
}
|
|
1088
1099
|
|
|
1089
|
-
if (previousIds.has(entity_id)) {
|
|
1100
|
+
if (previousIds.has(String(entity_id))) {
|
|
1090
1101
|
this.linkSubscribeToDocument(subscribe.id, `${subscribe.entity}:${entity_id}`)
|
|
1091
1102
|
continue
|
|
1092
1103
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CuboCrdtListTotals } from '../../types'
|
|
1
|
+
import type { CuboCrdtListTotals, CuboCrdtRowId } from '../../types'
|
|
2
2
|
|
|
3
3
|
export type CuboCrdtServerUnsubscribeDto = {
|
|
4
4
|
subscribe_id: string
|
|
@@ -24,7 +24,7 @@ export type CuboCrdtServerSubscribeDto = {
|
|
|
24
24
|
export type CuboCrdtServerSubscribe = {
|
|
25
25
|
id: string
|
|
26
26
|
client_id: string
|
|
27
|
-
row_ids:
|
|
27
|
+
row_ids: CuboCrdtRowId[]
|
|
28
28
|
totals: CuboCrdtListTotals
|
|
29
29
|
revision: number
|
|
30
30
|
} & Pick<CuboCrdtServerSubscribeDto, 'entity' | 'filters' | 'awareness' | 'paginated' | 'counted'>
|
package/src/types/index.ts
CHANGED
|
@@ -12,8 +12,18 @@ export type CuboCrdtSubscriptionStatus =
|
|
|
12
12
|
| 'ready'
|
|
13
13
|
| 'error'
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* A row's primary key, as it comes off the wire.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately NOT `number`. Rows are keyed by whatever the database column holds — uuid text in
|
|
19
|
+
* current schemas, integers in older ones — and the CRDT plane only ever compares and indexes ids, it
|
|
20
|
+
* never does arithmetic on them. Typing this as `number` is what let `Number(id)` / `Number.isFinite`
|
|
21
|
+
* creep into both ends of the sync, where every uuid failed the test and the row was silently dropped.
|
|
22
|
+
*/
|
|
23
|
+
export type CuboCrdtRowId = string | number
|
|
24
|
+
|
|
15
25
|
export type CuboCrdtListSyncData = {
|
|
16
|
-
ids:
|
|
26
|
+
ids: CuboCrdtRowId[]
|
|
17
27
|
totals: CuboCrdtListTotals
|
|
18
28
|
revision: number
|
|
19
29
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { CuboApiEntitiesMap } from '@cuboapp/types'
|
|
|
3
3
|
import {
|
|
4
4
|
CuboCrdtClient,
|
|
5
5
|
CuboCrdtClientOptions,
|
|
6
|
+
CuboCrdtRowId,
|
|
6
7
|
CuboCrdtServer,
|
|
7
8
|
CuboCrdtServerDocumentOrigin,
|
|
8
9
|
CuboCrdtServerOptions,
|
|
@@ -113,8 +114,10 @@ export function checkSubscribeIsSutable(origin: CuboCrdtServerDocumentOrigin, su
|
|
|
113
114
|
return noStrategyResolved || strategyOtherResolved || strategyClientResolved || strategySubscribeResolved
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
export function areCrdtListIdsEqual(left:
|
|
117
|
-
|
|
117
|
+
export function areCrdtListIdsEqual(left: CuboCrdtRowId[], right: CuboCrdtRowId[]) {
|
|
118
|
+
// Order matters (the list is ordered), but the id's TYPE does not: comparing as strings keeps a
|
|
119
|
+
// numeric id that made one round trip as text from reading as a changed list and forcing a resync.
|
|
120
|
+
return left.length === right.length && left.every((id, index) => String(id) === String(right[index]))
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
export function areCrdtListTotalsEqual(
|