@cuboapp/crdt 1.0.18 → 1.0.20
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/package.json
CHANGED
package/src/client/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ export class CuboCrdtClient<M> {
|
|
|
40
40
|
filters?: Record<string, any>
|
|
41
41
|
awareness?: boolean
|
|
42
42
|
paginated?: boolean
|
|
43
|
+
counted?: boolean
|
|
43
44
|
}
|
|
44
45
|
>()
|
|
45
46
|
private onReconnect = () => this.resubscribeAll()
|
|
@@ -145,7 +146,8 @@ export class CuboCrdtClient<M> {
|
|
|
145
146
|
entity: sub.entity,
|
|
146
147
|
filters: sub.filters,
|
|
147
148
|
awareness: sub.awareness,
|
|
148
|
-
paginated: sub.paginated
|
|
149
|
+
paginated: sub.paginated,
|
|
150
|
+
counted: sub.counted
|
|
149
151
|
}
|
|
150
152
|
},
|
|
151
153
|
{ wait: true, timeout: 10_000 }
|
|
@@ -176,6 +178,29 @@ export class CuboCrdtClient<M> {
|
|
|
176
178
|
return Number.isFinite(page) && page > 0 && Number.isFinite(limit) && limit > 0
|
|
177
179
|
}
|
|
178
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Подписка на КОЛИЧЕСТВО строк под фильтрами.
|
|
183
|
+
*
|
|
184
|
+
* Возвращает реактивное число и живёт как обычная подписка: сервер пересчитывает
|
|
185
|
+
* count на любую мутацию сущности и присылает только totals — строки не грузятся,
|
|
186
|
+
* CRDT-документы не создаются.
|
|
187
|
+
*
|
|
188
|
+
* Зачем: счётчику (оранжевая точка инбокса, число задач в «Неразобранном») нужно одно
|
|
189
|
+
* число, а обычная подписка ради него держала бы весь набор строк — память на сервере,
|
|
190
|
+
* трафик на каждое их изменение и документы, которые никто не читает.
|
|
191
|
+
*
|
|
192
|
+
* const { count, unsubscribe } = crdt.useCount('events', { filters: { recipient: 5 } })
|
|
193
|
+
*/
|
|
194
|
+
public useCount<K extends Extract<keyof M, string>>(entity: K | string, opts?: CuboCrdtClientUseOptions) {
|
|
195
|
+
const list = this.useList(entity, { ...(opts || {}), counted: true })
|
|
196
|
+
const totals = list.totals()
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
...list,
|
|
200
|
+
count: computed(() => Number(totals.value?.count || 0))
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
179
204
|
public useComputedList<K extends Extract<keyof M, string>>(entity: K) {
|
|
180
205
|
return computed(() => (this.store[entity]?.state.rows || []) as M[K][])
|
|
181
206
|
}
|
|
@@ -228,6 +253,7 @@ export class CuboCrdtClient<M> {
|
|
|
228
253
|
|
|
229
254
|
filters = filters !== undefined ? filters : opts?.filters
|
|
230
255
|
const paginated = this.resolvePaginated(filters, opts?.paginated)
|
|
256
|
+
const counted = !!opts?.counted
|
|
231
257
|
|
|
232
258
|
if (this.debug) {
|
|
233
259
|
console.log('[CRDT] subscribe', { entity, opts, filters })
|
|
@@ -256,7 +282,8 @@ export class CuboCrdtClient<M> {
|
|
|
256
282
|
storeKey,
|
|
257
283
|
filters,
|
|
258
284
|
awareness: opts?.awareness,
|
|
259
|
-
paginated
|
|
285
|
+
paginated,
|
|
286
|
+
counted
|
|
260
287
|
})
|
|
261
288
|
|
|
262
289
|
// подписываемся на бэке (если сокет ещё не подключён — переподпишемся по событию CONNECTED)
|
|
@@ -270,7 +297,8 @@ export class CuboCrdtClient<M> {
|
|
|
270
297
|
entity,
|
|
271
298
|
filters,
|
|
272
299
|
awareness: opts?.awareness,
|
|
273
|
-
paginated
|
|
300
|
+
paginated,
|
|
301
|
+
counted
|
|
274
302
|
}
|
|
275
303
|
},
|
|
276
304
|
{ wait: true, timeout: 10_000 }
|
|
@@ -489,7 +517,7 @@ export class CuboCrdtClient<M> {
|
|
|
489
517
|
.request(
|
|
490
518
|
{
|
|
491
519
|
method: CUBO_CRDT_EVENT.UPGRADE,
|
|
492
|
-
data: { subscribe_id, filters, paginated: sub?.paginated }
|
|
520
|
+
data: { subscribe_id, filters, paginated: sub?.paginated, counted: sub?.counted }
|
|
493
521
|
},
|
|
494
522
|
{ wait: true, timeout: 10_000 }
|
|
495
523
|
)
|
|
@@ -27,6 +27,11 @@ export type CuboCrdtClientUseOptions = CuboCrdtClientBaseOptions & {
|
|
|
27
27
|
filters?: Record<string, any>
|
|
28
28
|
awareness?: boolean
|
|
29
29
|
paginated?: boolean
|
|
30
|
+
/**
|
|
31
|
+
* Подписка только на КОЛИЧЕСТВО строк под фильтрами: строки не грузятся и документы
|
|
32
|
+
* не создаются, приезжает лишь totals. Для счётчиков и индикаторов — см. useCount.
|
|
33
|
+
*/
|
|
34
|
+
counted?: boolean
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
export type CuboCrdtClientSubscribeEvent = {
|
|
@@ -64,7 +64,9 @@ export class CuboCrdtServerDocument {
|
|
|
64
64
|
const body = pick(row, origin.keys ?? Object.keys(row))
|
|
65
65
|
|
|
66
66
|
if (Object.keys(body).length) {
|
|
67
|
-
this.debounceStore(body, origin)
|
|
67
|
+
this.debounceStore(body, origin).catch((error) => {
|
|
68
|
+
console.error('[CRDT] store document', this.name, error)
|
|
69
|
+
})
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
})
|
|
@@ -102,7 +104,7 @@ export class CuboCrdtServerDocument {
|
|
|
102
104
|
this.persistedState = cloneDeep(row)
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
write(dto: object, origin?: CuboCrdtServerDocumentOrigin) {
|
|
106
108
|
const map = this.getMap()
|
|
107
109
|
|
|
108
110
|
// Пропускаем скалярные ключи с тем же значением: Y.Map.set всегда создаёт новый Item и
|
|
@@ -138,5 +140,6 @@ export class CuboCrdtServerDocument {
|
|
|
138
140
|
await this.opts?.onStore?.(body, origin, { previousRow, row })
|
|
139
141
|
this.setPersistedState(row)
|
|
140
142
|
}
|
|
143
|
+
|
|
141
144
|
public debounceStore = debounce(this.store.bind(this), 300)
|
|
142
145
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -49,7 +49,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
49
49
|
this.entities.forEach((e) => this.subscribesByEntity.set(e, new Set()))
|
|
50
50
|
|
|
51
51
|
this.ws.registerHandler(CUBO_CRDT_EVENT.SUBSCRIBE, async ({ client, message }) => {
|
|
52
|
-
const { subscribe_id: id, entity, filters, awareness = false, paginated } = message.data as CuboCrdtServerSubscribeDto
|
|
52
|
+
const { subscribe_id: id, entity, filters, awareness = false, paginated, counted } = message.data as CuboCrdtServerSubscribeDto
|
|
53
53
|
|
|
54
54
|
if (this.debug) {
|
|
55
55
|
console.log('[CRDT] subscribe', entity, filters)
|
|
@@ -67,6 +67,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
67
67
|
filters: cloneDeep(filters || {}),
|
|
68
68
|
awareness,
|
|
69
69
|
paginated: this.resolvePaginated(filters, paginated),
|
|
70
|
+
counted: !!counted,
|
|
70
71
|
row_ids: [],
|
|
71
72
|
totals: {},
|
|
72
73
|
revision: 0
|
|
@@ -87,7 +88,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
87
88
|
})
|
|
88
89
|
|
|
89
90
|
this.ws.registerHandler(CUBO_CRDT_EVENT.UPGRADE, async ({ client, message }) => {
|
|
90
|
-
const { subscribe_id: id, entity, filters, paginated } = message.data as CuboCrdtServerSubscribeDto
|
|
91
|
+
const { subscribe_id: id, entity, filters, paginated, counted } = message.data as CuboCrdtServerSubscribeDto
|
|
91
92
|
|
|
92
93
|
if (this.debug) {
|
|
93
94
|
console.log('[CRDT] upgrade', entity, filters)
|
|
@@ -99,7 +100,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
// обновляем подписку
|
|
102
|
-
await this.upgradeSubscribe(client, subscribe, { filters, paginated })
|
|
103
|
+
await this.upgradeSubscribe(client, subscribe, { filters, paginated, counted })
|
|
103
104
|
|
|
104
105
|
return { subscribe_id: id, revision: subscribe.revision }
|
|
105
106
|
})
|
|
@@ -523,6 +524,13 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
523
524
|
}
|
|
524
525
|
|
|
525
526
|
private shouldRefreshSubscribe(entity: E, subscribe: CuboCrdtServerSubscribe, mutation: CuboCrdtMutation<M[E]>) {
|
|
527
|
+
// Счётчик пересчитываем на любую мутацию сущности: количество — свойство ВЫБОРКИ,
|
|
528
|
+
// а не строки, и по одному событию понять, изменилось ли оно, нельзя. Строк мы при
|
|
529
|
+
// этом не держим, поэтому пересчёт стоит один запрос count, а не выгрузку набора.
|
|
530
|
+
if (subscribe.counted) {
|
|
531
|
+
return true
|
|
532
|
+
}
|
|
533
|
+
|
|
526
534
|
if (!subscribe.paginated) {
|
|
527
535
|
return false
|
|
528
536
|
}
|
|
@@ -582,7 +590,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
582
590
|
public invalidate(entity: E) {
|
|
583
591
|
for (const subscribe_id of this.subscribesByEntity.get(entity) || []) {
|
|
584
592
|
const subscribe = this.subscribes.get(subscribe_id)
|
|
585
|
-
if (subscribe?.paginated) {
|
|
593
|
+
if (subscribe?.paginated || subscribe?.counted) {
|
|
586
594
|
this.requestSubscribeRefresh(subscribe_id)
|
|
587
595
|
}
|
|
588
596
|
}
|
|
@@ -953,6 +961,33 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
953
961
|
return false
|
|
954
962
|
}
|
|
955
963
|
|
|
964
|
+
// Счётчику строки не нужны: документы не создаём и не связываем, храним только
|
|
965
|
+
// totals. Иначе подписка ради одного числа тянула бы весь набор в память сервера
|
|
966
|
+
// и рассылала бы клиенту каждое его изменение.
|
|
967
|
+
if (subscribe.counted) {
|
|
968
|
+
const totalsChanged = !areCrdtListTotalsEqual(subscribe.totals, result.totals)
|
|
969
|
+
|
|
970
|
+
subscribe.totals = result.totals
|
|
971
|
+
|
|
972
|
+
if (!forceSync && !totalsChanged) {
|
|
973
|
+
return true
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
subscribe.revision += 1
|
|
977
|
+
|
|
978
|
+
// Тот же sync, что и у пагинированной подписки, только со всегда пустым списком id:
|
|
979
|
+
// клиент уже умеет его принимать и обновлять totals/revision — отдельный тип
|
|
980
|
+
// сообщения пришлось бы поддерживать в обоих концах.
|
|
981
|
+
this.sendToClient(subscribe.client_id, {
|
|
982
|
+
action: 'sync',
|
|
983
|
+
entity: subscribe.entity,
|
|
984
|
+
subscribe_id: subscribe.id,
|
|
985
|
+
data: { ids: [], totals: subscribe.totals, revision: subscribe.revision }
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
return true
|
|
989
|
+
}
|
|
990
|
+
|
|
956
991
|
const rowsById = new Map<number, any>()
|
|
957
992
|
const nextIds: number[] = []
|
|
958
993
|
|
|
@@ -1028,7 +1063,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
1028
1063
|
private async upgradeSubscribe(
|
|
1029
1064
|
client: WsServerSocket,
|
|
1030
1065
|
subscribe: CuboCrdtServerSubscribe,
|
|
1031
|
-
upgrade: Partial<Pick<CuboCrdtServerSubscribe, 'filters' | 'paginated'>>
|
|
1066
|
+
upgrade: Partial<Pick<CuboCrdtServerSubscribe, 'filters' | 'paginated' | 'counted'>>
|
|
1032
1067
|
) {
|
|
1033
1068
|
if (this.debug) {
|
|
1034
1069
|
console.log('[CRDT] upgrade subscribe ' + client.id, subscribe)
|
|
@@ -11,6 +11,14 @@ export type CuboCrdtServerSubscribeDto = {
|
|
|
11
11
|
}
|
|
12
12
|
awareness?: boolean
|
|
13
13
|
paginated?: boolean
|
|
14
|
+
/**
|
|
15
|
+
* Подписка только на КОЛИЧЕСТВО строк под фильтрами.
|
|
16
|
+
*
|
|
17
|
+
* Документы при этом не создаются вовсе: клиенту нужно одно число (счётчик,
|
|
18
|
+
* индикатор непрочитанного), а держать ради него весь набор строк — держать
|
|
19
|
+
* лишнюю память на сервере и лишний трафик на каждое их изменение.
|
|
20
|
+
*/
|
|
21
|
+
counted?: boolean
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
export type CuboCrdtServerSubscribe = {
|
|
@@ -19,7 +27,7 @@ export type CuboCrdtServerSubscribe = {
|
|
|
19
27
|
row_ids: number[]
|
|
20
28
|
totals: CuboCrdtListTotals
|
|
21
29
|
revision: number
|
|
22
|
-
} & Pick<CuboCrdtServerSubscribeDto, 'entity' | 'filters' | 'awareness' | 'paginated'>
|
|
30
|
+
} & Pick<CuboCrdtServerSubscribeDto, 'entity' | 'filters' | 'awareness' | 'paginated' | 'counted'>
|
|
23
31
|
|
|
24
32
|
export type CuboCrdtSubscribeRefreshState = {
|
|
25
33
|
dirty: boolean
|