hyperstack-typescript 0.3.2 → 0.3.4
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/index.d.ts +95 -7
- package/dist/index.esm.js +529 -25
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +531 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25,6 +25,12 @@ function isGzipData(data) {
|
|
|
25
25
|
function isSnapshotFrame(frame) {
|
|
26
26
|
return frame.op === 'snapshot';
|
|
27
27
|
}
|
|
28
|
+
function isSubscribedFrame(frame) {
|
|
29
|
+
return frame.op === 'subscribed';
|
|
30
|
+
}
|
|
31
|
+
function isEntityFrame(frame) {
|
|
32
|
+
return ['create', 'upsert', 'patch', 'delete'].includes(frame.op);
|
|
33
|
+
}
|
|
28
34
|
function parseFrame(data) {
|
|
29
35
|
if (typeof data === 'string') {
|
|
30
36
|
return JSON.parse(data);
|
|
@@ -133,7 +139,6 @@ class ConnectionManager {
|
|
|
133
139
|
this.notifyFrameHandlers(frame);
|
|
134
140
|
}
|
|
135
141
|
catch (error) {
|
|
136
|
-
console.error('[hyperstack] Error parsing frame:', error);
|
|
137
142
|
this.updateState('error', 'Failed to parse frame from server');
|
|
138
143
|
}
|
|
139
144
|
};
|
|
@@ -172,21 +177,15 @@ class ConnectionManager {
|
|
|
172
177
|
const subKey = this.makeSubKey(subscription);
|
|
173
178
|
if (this.currentState === 'connected' && this.ws?.readyState === WebSocket.OPEN) {
|
|
174
179
|
if (this.activeSubscriptions.has(subKey)) {
|
|
175
|
-
console.log('[hyperstack] Skipping already active subscription:', subKey);
|
|
176
180
|
return;
|
|
177
181
|
}
|
|
178
182
|
const subMsg = { type: 'subscribe', ...subscription };
|
|
179
|
-
console.log('[hyperstack] Sending subscribe:', subKey);
|
|
180
183
|
this.ws.send(JSON.stringify(subMsg));
|
|
181
184
|
this.activeSubscriptions.add(subKey);
|
|
182
185
|
}
|
|
183
186
|
else {
|
|
184
187
|
const alreadyQueued = this.subscriptionQueue.some((s) => this.makeSubKey(s) === subKey);
|
|
185
|
-
if (alreadyQueued) {
|
|
186
|
-
console.log('[hyperstack] Skipping duplicate queue entry:', subKey);
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
console.log('[hyperstack] Queuing subscription:', subKey, '| Queue:', this.subscriptionQueue.map(s => this.makeSubKey(s)));
|
|
188
|
+
if (!alreadyQueued) {
|
|
190
189
|
this.subscriptionQueue.push(subscription);
|
|
191
190
|
}
|
|
192
191
|
}
|
|
@@ -209,7 +208,6 @@ class ConnectionManager {
|
|
|
209
208
|
return `${subscription.view}:${subscription.key ?? '*'}:${subscription.partition ?? ''}`;
|
|
210
209
|
}
|
|
211
210
|
flushSubscriptionQueue() {
|
|
212
|
-
console.log('[hyperstack] Flushing subscription queue:', this.subscriptionQueue.map(s => this.makeSubKey(s)));
|
|
213
211
|
while (this.subscriptionQueue.length > 0) {
|
|
214
212
|
const sub = this.subscriptionQueue.shift();
|
|
215
213
|
if (sub) {
|
|
@@ -218,7 +216,6 @@ class ConnectionManager {
|
|
|
218
216
|
}
|
|
219
217
|
}
|
|
220
218
|
resubscribeActive() {
|
|
221
|
-
console.log('[hyperstack] Resubscribing active:', Array.from(this.activeSubscriptions));
|
|
222
219
|
for (const subKey of this.activeSubscriptions) {
|
|
223
220
|
const [view, key, partition] = subKey.split(':');
|
|
224
221
|
const subscription = {
|
|
@@ -228,7 +225,6 @@ class ConnectionManager {
|
|
|
228
225
|
};
|
|
229
226
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
230
227
|
const subMsg = { type: 'subscribe', ...subscription };
|
|
231
|
-
console.log('[hyperstack] Resubscribe sending:', subKey);
|
|
232
228
|
this.ws.send(JSON.stringify(subMsg));
|
|
233
229
|
}
|
|
234
230
|
}
|
|
@@ -281,11 +277,11 @@ class ConnectionManager {
|
|
|
281
277
|
}
|
|
282
278
|
}
|
|
283
279
|
|
|
284
|
-
function isObject(item) {
|
|
280
|
+
function isObject$1(item) {
|
|
285
281
|
return item !== null && typeof item === 'object' && !Array.isArray(item);
|
|
286
282
|
}
|
|
287
|
-
function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
288
|
-
if (!isObject(target) || !isObject(source)) {
|
|
283
|
+
function deepMergeWithAppend$1(target, source, appendPaths, currentPath = '') {
|
|
284
|
+
if (!isObject$1(target) || !isObject$1(source)) {
|
|
289
285
|
return source;
|
|
290
286
|
}
|
|
291
287
|
const result = { ...target };
|
|
@@ -301,8 +297,8 @@ function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
|
301
297
|
result[key] = sourceValue;
|
|
302
298
|
}
|
|
303
299
|
}
|
|
304
|
-
else if (isObject(sourceValue) && isObject(targetValue)) {
|
|
305
|
-
result[key] = deepMergeWithAppend(targetValue, sourceValue, appendPaths, fieldPath);
|
|
300
|
+
else if (isObject$1(sourceValue) && isObject$1(targetValue)) {
|
|
301
|
+
result[key] = deepMergeWithAppend$1(targetValue, sourceValue, appendPaths, fieldPath);
|
|
306
302
|
}
|
|
307
303
|
else {
|
|
308
304
|
result[key] = sourceValue;
|
|
@@ -312,20 +308,107 @@ function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
|
312
308
|
}
|
|
313
309
|
class FrameProcessor {
|
|
314
310
|
constructor(storage, config = {}) {
|
|
311
|
+
this.pendingUpdates = [];
|
|
312
|
+
this.flushTimer = null;
|
|
313
|
+
this.isProcessing = false;
|
|
315
314
|
this.storage = storage;
|
|
316
315
|
this.maxEntriesPerView = config.maxEntriesPerView === undefined
|
|
317
316
|
? DEFAULT_MAX_ENTRIES_PER_VIEW
|
|
318
317
|
: config.maxEntriesPerView;
|
|
318
|
+
this.flushIntervalMs = config.flushIntervalMs ?? 0;
|
|
319
319
|
}
|
|
320
320
|
handleFrame(frame) {
|
|
321
|
-
if (
|
|
321
|
+
if (this.flushIntervalMs === 0) {
|
|
322
|
+
this.processFrame(frame);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
this.pendingUpdates.push({ frame });
|
|
326
|
+
this.scheduleFlush();
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Immediately flush all pending updates.
|
|
330
|
+
* Useful for ensuring all updates are processed before reading state.
|
|
331
|
+
*/
|
|
332
|
+
flush() {
|
|
333
|
+
if (this.flushTimer !== null) {
|
|
334
|
+
clearTimeout(this.flushTimer);
|
|
335
|
+
this.flushTimer = null;
|
|
336
|
+
}
|
|
337
|
+
this.flushPendingUpdates();
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Clean up any pending timers. Call when disposing the processor.
|
|
341
|
+
*/
|
|
342
|
+
dispose() {
|
|
343
|
+
if (this.flushTimer !== null) {
|
|
344
|
+
clearTimeout(this.flushTimer);
|
|
345
|
+
this.flushTimer = null;
|
|
346
|
+
}
|
|
347
|
+
this.pendingUpdates = [];
|
|
348
|
+
}
|
|
349
|
+
scheduleFlush() {
|
|
350
|
+
if (this.flushTimer !== null) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
this.flushTimer = setTimeout(() => {
|
|
354
|
+
this.flushTimer = null;
|
|
355
|
+
this.flushPendingUpdates();
|
|
356
|
+
}, this.flushIntervalMs);
|
|
357
|
+
}
|
|
358
|
+
flushPendingUpdates() {
|
|
359
|
+
if (this.isProcessing || this.pendingUpdates.length === 0) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
this.isProcessing = true;
|
|
363
|
+
const batch = this.pendingUpdates;
|
|
364
|
+
this.pendingUpdates = [];
|
|
365
|
+
const viewsToEnforce = new Set();
|
|
366
|
+
for (const { frame } of batch) {
|
|
367
|
+
const viewPath = this.processFrameWithoutEnforce(frame);
|
|
368
|
+
if (viewPath) {
|
|
369
|
+
viewsToEnforce.add(viewPath);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
viewsToEnforce.forEach((viewPath) => {
|
|
373
|
+
this.enforceMaxEntries(viewPath);
|
|
374
|
+
});
|
|
375
|
+
this.isProcessing = false;
|
|
376
|
+
}
|
|
377
|
+
processFrame(frame) {
|
|
378
|
+
if (isSubscribedFrame(frame)) {
|
|
379
|
+
this.handleSubscribedFrame(frame);
|
|
380
|
+
}
|
|
381
|
+
else if (isSnapshotFrame(frame)) {
|
|
322
382
|
this.handleSnapshotFrame(frame);
|
|
323
383
|
}
|
|
324
384
|
else {
|
|
325
385
|
this.handleEntityFrame(frame);
|
|
326
386
|
}
|
|
327
387
|
}
|
|
388
|
+
processFrameWithoutEnforce(frame) {
|
|
389
|
+
if (isSubscribedFrame(frame)) {
|
|
390
|
+
this.handleSubscribedFrame(frame);
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
else if (isSnapshotFrame(frame)) {
|
|
394
|
+
this.handleSnapshotFrameWithoutEnforce(frame);
|
|
395
|
+
return frame.entity;
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
this.handleEntityFrameWithoutEnforce(frame);
|
|
399
|
+
return frame.entity;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
handleSubscribedFrame(frame) {
|
|
403
|
+
if (this.storage.setViewConfig && frame.sort) {
|
|
404
|
+
this.storage.setViewConfig(frame.view, { sort: frame.sort });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
328
407
|
handleSnapshotFrame(frame) {
|
|
408
|
+
this.handleSnapshotFrameWithoutEnforce(frame);
|
|
409
|
+
this.enforceMaxEntries(frame.entity);
|
|
410
|
+
}
|
|
411
|
+
handleSnapshotFrameWithoutEnforce(frame) {
|
|
329
412
|
const viewPath = frame.entity;
|
|
330
413
|
for (const entity of frame.data) {
|
|
331
414
|
const previousValue = this.storage.get(viewPath, entity.key);
|
|
@@ -337,16 +420,18 @@ class FrameProcessor {
|
|
|
337
420
|
});
|
|
338
421
|
this.emitRichUpdate(viewPath, entity.key, previousValue, entity.data, 'upsert');
|
|
339
422
|
}
|
|
340
|
-
this.enforceMaxEntries(viewPath);
|
|
341
423
|
}
|
|
342
424
|
handleEntityFrame(frame) {
|
|
425
|
+
this.handleEntityFrameWithoutEnforce(frame);
|
|
426
|
+
this.enforceMaxEntries(frame.entity);
|
|
427
|
+
}
|
|
428
|
+
handleEntityFrameWithoutEnforce(frame) {
|
|
343
429
|
const viewPath = frame.entity;
|
|
344
430
|
const previousValue = this.storage.get(viewPath, frame.key);
|
|
345
431
|
switch (frame.op) {
|
|
346
432
|
case 'create':
|
|
347
433
|
case 'upsert':
|
|
348
434
|
this.storage.set(viewPath, frame.key, frame.data);
|
|
349
|
-
this.enforceMaxEntries(viewPath);
|
|
350
435
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
351
436
|
type: 'upsert',
|
|
352
437
|
key: frame.key,
|
|
@@ -358,10 +443,9 @@ class FrameProcessor {
|
|
|
358
443
|
const existing = this.storage.get(viewPath, frame.key);
|
|
359
444
|
const appendPaths = frame.append ?? [];
|
|
360
445
|
const merged = existing
|
|
361
|
-
? deepMergeWithAppend(existing, frame.data, appendPaths)
|
|
446
|
+
? deepMergeWithAppend$1(existing, frame.data, appendPaths)
|
|
362
447
|
: frame.data;
|
|
363
448
|
this.storage.set(viewPath, frame.key, merged);
|
|
364
|
-
this.enforceMaxEntries(viewPath);
|
|
365
449
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
366
450
|
type: 'patch',
|
|
367
451
|
key: frame.key,
|
|
@@ -400,7 +484,7 @@ class FrameProcessor {
|
|
|
400
484
|
}
|
|
401
485
|
}
|
|
402
486
|
|
|
403
|
-
class ViewData {
|
|
487
|
+
let ViewData$1 = class ViewData {
|
|
404
488
|
constructor() {
|
|
405
489
|
this.entities = new Map();
|
|
406
490
|
this.accessOrder = [];
|
|
@@ -454,7 +538,7 @@ class ViewData {
|
|
|
454
538
|
this.entities.clear();
|
|
455
539
|
this.accessOrder = [];
|
|
456
540
|
}
|
|
457
|
-
}
|
|
541
|
+
};
|
|
458
542
|
class MemoryAdapter {
|
|
459
543
|
constructor(_config = {}) {
|
|
460
544
|
this.views = new Map();
|
|
@@ -502,7 +586,7 @@ class MemoryAdapter {
|
|
|
502
586
|
set(viewPath, key, data) {
|
|
503
587
|
let view = this.views.get(viewPath);
|
|
504
588
|
if (!view) {
|
|
505
|
-
view = new ViewData();
|
|
589
|
+
view = new ViewData$1();
|
|
506
590
|
this.views.set(viewPath, view);
|
|
507
591
|
}
|
|
508
592
|
view.set(key, data);
|
|
@@ -843,9 +927,430 @@ class HyperStack {
|
|
|
843
927
|
}
|
|
844
928
|
}
|
|
845
929
|
|
|
930
|
+
function getNestedValue(obj, path) {
|
|
931
|
+
let current = obj;
|
|
932
|
+
for (const segment of path) {
|
|
933
|
+
if (current === null || current === undefined)
|
|
934
|
+
return undefined;
|
|
935
|
+
if (typeof current !== 'object')
|
|
936
|
+
return undefined;
|
|
937
|
+
current = current[segment];
|
|
938
|
+
}
|
|
939
|
+
return current;
|
|
940
|
+
}
|
|
941
|
+
function compareSortValues(a, b) {
|
|
942
|
+
if (a === b)
|
|
943
|
+
return 0;
|
|
944
|
+
if (a === undefined || a === null)
|
|
945
|
+
return -1;
|
|
946
|
+
if (b === undefined || b === null)
|
|
947
|
+
return 1;
|
|
948
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
949
|
+
return a - b;
|
|
950
|
+
}
|
|
951
|
+
if (typeof a === 'string' && typeof b === 'string') {
|
|
952
|
+
return a.localeCompare(b);
|
|
953
|
+
}
|
|
954
|
+
if (typeof a === 'boolean' && typeof b === 'boolean') {
|
|
955
|
+
return (a ? 1 : 0) - (b ? 1 : 0);
|
|
956
|
+
}
|
|
957
|
+
return String(a).localeCompare(String(b));
|
|
958
|
+
}
|
|
959
|
+
class ViewData {
|
|
960
|
+
constructor(sortConfig) {
|
|
961
|
+
this.entities = new Map();
|
|
962
|
+
this.accessOrder = [];
|
|
963
|
+
this.sortedKeys = [];
|
|
964
|
+
this.sortConfig = sortConfig;
|
|
965
|
+
}
|
|
966
|
+
get(key) {
|
|
967
|
+
return this.entities.get(key);
|
|
968
|
+
}
|
|
969
|
+
set(key, value) {
|
|
970
|
+
const isNew = !this.entities.has(key);
|
|
971
|
+
this.entities.set(key, value);
|
|
972
|
+
if (this.sortConfig) {
|
|
973
|
+
this.updateSortedPosition(key, value, isNew);
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
if (isNew) {
|
|
977
|
+
this.accessOrder.push(key);
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
this.touch(key);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
updateSortedPosition(key, value, isNew) {
|
|
985
|
+
if (!isNew) {
|
|
986
|
+
const existingIdx = this.sortedKeys.indexOf(key);
|
|
987
|
+
if (existingIdx !== -1) {
|
|
988
|
+
this.sortedKeys.splice(existingIdx, 1);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const sortValue = getNestedValue(value, this.sortConfig.field);
|
|
992
|
+
const isDesc = this.sortConfig.order === 'desc';
|
|
993
|
+
let insertIdx = this.binarySearchInsertPosition(sortValue, key, isDesc);
|
|
994
|
+
this.sortedKeys.splice(insertIdx, 0, key);
|
|
995
|
+
}
|
|
996
|
+
binarySearchInsertPosition(sortValue, key, isDesc) {
|
|
997
|
+
let low = 0;
|
|
998
|
+
let high = this.sortedKeys.length;
|
|
999
|
+
while (low < high) {
|
|
1000
|
+
const mid = Math.floor((low + high) / 2);
|
|
1001
|
+
const midKey = this.sortedKeys[mid];
|
|
1002
|
+
const midEntity = this.entities.get(midKey);
|
|
1003
|
+
const midValue = getNestedValue(midEntity, this.sortConfig.field);
|
|
1004
|
+
let cmp = compareSortValues(sortValue, midValue);
|
|
1005
|
+
if (isDesc)
|
|
1006
|
+
cmp = -cmp;
|
|
1007
|
+
if (cmp === 0) {
|
|
1008
|
+
cmp = key.localeCompare(midKey);
|
|
1009
|
+
}
|
|
1010
|
+
if (cmp < 0) {
|
|
1011
|
+
high = mid;
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
low = mid + 1;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return low;
|
|
1018
|
+
}
|
|
1019
|
+
delete(key) {
|
|
1020
|
+
if (this.sortConfig) {
|
|
1021
|
+
const idx = this.sortedKeys.indexOf(key);
|
|
1022
|
+
if (idx !== -1) {
|
|
1023
|
+
this.sortedKeys.splice(idx, 1);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
const idx = this.accessOrder.indexOf(key);
|
|
1028
|
+
if (idx !== -1) {
|
|
1029
|
+
this.accessOrder.splice(idx, 1);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return this.entities.delete(key);
|
|
1033
|
+
}
|
|
1034
|
+
has(key) {
|
|
1035
|
+
return this.entities.has(key);
|
|
1036
|
+
}
|
|
1037
|
+
values() {
|
|
1038
|
+
if (this.sortConfig) {
|
|
1039
|
+
return this.sortedKeys.map(k => this.entities.get(k));
|
|
1040
|
+
}
|
|
1041
|
+
return Array.from(this.entities.values());
|
|
1042
|
+
}
|
|
1043
|
+
keys() {
|
|
1044
|
+
if (this.sortConfig) {
|
|
1045
|
+
return [...this.sortedKeys];
|
|
1046
|
+
}
|
|
1047
|
+
return Array.from(this.entities.keys());
|
|
1048
|
+
}
|
|
1049
|
+
get size() {
|
|
1050
|
+
return this.entities.size;
|
|
1051
|
+
}
|
|
1052
|
+
touch(key) {
|
|
1053
|
+
if (this.sortConfig)
|
|
1054
|
+
return;
|
|
1055
|
+
const idx = this.accessOrder.indexOf(key);
|
|
1056
|
+
if (idx !== -1) {
|
|
1057
|
+
this.accessOrder.splice(idx, 1);
|
|
1058
|
+
this.accessOrder.push(key);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
evictOldest() {
|
|
1062
|
+
if (this.sortConfig) {
|
|
1063
|
+
const oldest = this.sortedKeys.pop();
|
|
1064
|
+
if (oldest !== undefined) {
|
|
1065
|
+
this.entities.delete(oldest);
|
|
1066
|
+
}
|
|
1067
|
+
return oldest;
|
|
1068
|
+
}
|
|
1069
|
+
const oldest = this.accessOrder.shift();
|
|
1070
|
+
if (oldest !== undefined) {
|
|
1071
|
+
this.entities.delete(oldest);
|
|
1072
|
+
}
|
|
1073
|
+
return oldest;
|
|
1074
|
+
}
|
|
1075
|
+
setSortConfig(config) {
|
|
1076
|
+
if (this.sortConfig)
|
|
1077
|
+
return;
|
|
1078
|
+
this.sortConfig = config;
|
|
1079
|
+
this.rebuildSortedKeys();
|
|
1080
|
+
}
|
|
1081
|
+
rebuildSortedKeys() {
|
|
1082
|
+
if (!this.sortConfig)
|
|
1083
|
+
return;
|
|
1084
|
+
const entries = Array.from(this.entities.entries());
|
|
1085
|
+
const isDesc = this.sortConfig.order === 'desc';
|
|
1086
|
+
entries.sort((a, b) => {
|
|
1087
|
+
const aValue = getNestedValue(a[1], this.sortConfig.field);
|
|
1088
|
+
const bValue = getNestedValue(b[1], this.sortConfig.field);
|
|
1089
|
+
let cmp = compareSortValues(aValue, bValue);
|
|
1090
|
+
if (isDesc)
|
|
1091
|
+
cmp = -cmp;
|
|
1092
|
+
if (cmp === 0) {
|
|
1093
|
+
cmp = a[0].localeCompare(b[0]);
|
|
1094
|
+
}
|
|
1095
|
+
return cmp;
|
|
1096
|
+
});
|
|
1097
|
+
this.sortedKeys = entries.map(([k]) => k);
|
|
1098
|
+
this.accessOrder = [];
|
|
1099
|
+
}
|
|
1100
|
+
getSortConfig() {
|
|
1101
|
+
return this.sortConfig;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
function isObject(item) {
|
|
1105
|
+
return item !== null && typeof item === 'object' && !Array.isArray(item);
|
|
1106
|
+
}
|
|
1107
|
+
function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
1108
|
+
if (!isObject(target) || !isObject(source)) {
|
|
1109
|
+
return source;
|
|
1110
|
+
}
|
|
1111
|
+
const result = { ...target };
|
|
1112
|
+
for (const key in source) {
|
|
1113
|
+
const sourceValue = source[key];
|
|
1114
|
+
const targetValue = result[key];
|
|
1115
|
+
const fieldPath = currentPath ? `${currentPath}.${key}` : key;
|
|
1116
|
+
if (Array.isArray(sourceValue) && Array.isArray(targetValue)) {
|
|
1117
|
+
if (appendPaths.includes(fieldPath)) {
|
|
1118
|
+
result[key] = [...targetValue, ...sourceValue];
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
result[key] = sourceValue;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
else if (isObject(sourceValue) && isObject(targetValue)) {
|
|
1125
|
+
result[key] = deepMergeWithAppend(targetValue, sourceValue, appendPaths, fieldPath);
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
result[key] = sourceValue;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return result;
|
|
1132
|
+
}
|
|
1133
|
+
class EntityStore {
|
|
1134
|
+
constructor(config = {}) {
|
|
1135
|
+
this.views = new Map();
|
|
1136
|
+
this.viewConfigs = new Map();
|
|
1137
|
+
this.updateCallbacks = new Set();
|
|
1138
|
+
this.richUpdateCallbacks = new Set();
|
|
1139
|
+
this.maxEntriesPerView = config.maxEntriesPerView === undefined
|
|
1140
|
+
? DEFAULT_MAX_ENTRIES_PER_VIEW
|
|
1141
|
+
: config.maxEntriesPerView;
|
|
1142
|
+
}
|
|
1143
|
+
enforceMaxEntries(viewData) {
|
|
1144
|
+
if (this.maxEntriesPerView === null)
|
|
1145
|
+
return;
|
|
1146
|
+
while (viewData.size > this.maxEntriesPerView) {
|
|
1147
|
+
viewData.evictOldest();
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
handleFrame(frame) {
|
|
1151
|
+
if (isSubscribedFrame(frame)) {
|
|
1152
|
+
this.handleSubscribedFrame(frame);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
if (isSnapshotFrame(frame)) {
|
|
1156
|
+
this.handleSnapshotFrame(frame);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
this.handleEntityFrame(frame);
|
|
1160
|
+
}
|
|
1161
|
+
handleSubscribedFrame(frame) {
|
|
1162
|
+
const viewPath = frame.view;
|
|
1163
|
+
const config = {};
|
|
1164
|
+
if (frame.sort) {
|
|
1165
|
+
config.sort = frame.sort;
|
|
1166
|
+
}
|
|
1167
|
+
this.viewConfigs.set(viewPath, config);
|
|
1168
|
+
const existingView = this.views.get(viewPath);
|
|
1169
|
+
if (existingView && frame.sort) {
|
|
1170
|
+
existingView.setSortConfig(frame.sort);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
handleSnapshotFrame(frame) {
|
|
1174
|
+
const viewPath = frame.entity;
|
|
1175
|
+
let viewData = this.views.get(viewPath);
|
|
1176
|
+
const viewConfig = this.viewConfigs.get(viewPath);
|
|
1177
|
+
if (!viewData) {
|
|
1178
|
+
viewData = new ViewData(viewConfig?.sort);
|
|
1179
|
+
this.views.set(viewPath, viewData);
|
|
1180
|
+
}
|
|
1181
|
+
for (const entity of frame.data) {
|
|
1182
|
+
const previousValue = viewData.get(entity.key);
|
|
1183
|
+
viewData.set(entity.key, entity.data);
|
|
1184
|
+
this.notifyUpdate(viewPath, entity.key, {
|
|
1185
|
+
type: 'upsert',
|
|
1186
|
+
key: entity.key,
|
|
1187
|
+
data: entity.data,
|
|
1188
|
+
});
|
|
1189
|
+
this.notifyRichUpdate(viewPath, entity.key, previousValue, entity.data, 'upsert');
|
|
1190
|
+
}
|
|
1191
|
+
this.enforceMaxEntries(viewData);
|
|
1192
|
+
}
|
|
1193
|
+
handleEntityFrame(frame) {
|
|
1194
|
+
const viewPath = frame.entity;
|
|
1195
|
+
let viewData = this.views.get(viewPath);
|
|
1196
|
+
const viewConfig = this.viewConfigs.get(viewPath);
|
|
1197
|
+
if (!viewData) {
|
|
1198
|
+
viewData = new ViewData(viewConfig?.sort);
|
|
1199
|
+
this.views.set(viewPath, viewData);
|
|
1200
|
+
}
|
|
1201
|
+
const previousValue = viewData.get(frame.key);
|
|
1202
|
+
switch (frame.op) {
|
|
1203
|
+
case 'create':
|
|
1204
|
+
case 'upsert':
|
|
1205
|
+
viewData.set(frame.key, frame.data);
|
|
1206
|
+
this.enforceMaxEntries(viewData);
|
|
1207
|
+
this.notifyUpdate(viewPath, frame.key, {
|
|
1208
|
+
type: 'upsert',
|
|
1209
|
+
key: frame.key,
|
|
1210
|
+
data: frame.data,
|
|
1211
|
+
});
|
|
1212
|
+
this.notifyRichUpdate(viewPath, frame.key, previousValue, frame.data, frame.op);
|
|
1213
|
+
break;
|
|
1214
|
+
case 'patch': {
|
|
1215
|
+
const existing = viewData.get(frame.key);
|
|
1216
|
+
const appendPaths = frame.append ?? [];
|
|
1217
|
+
const merged = existing
|
|
1218
|
+
? deepMergeWithAppend(existing, frame.data, appendPaths)
|
|
1219
|
+
: frame.data;
|
|
1220
|
+
viewData.set(frame.key, merged);
|
|
1221
|
+
this.enforceMaxEntries(viewData);
|
|
1222
|
+
this.notifyUpdate(viewPath, frame.key, {
|
|
1223
|
+
type: 'patch',
|
|
1224
|
+
key: frame.key,
|
|
1225
|
+
data: frame.data,
|
|
1226
|
+
});
|
|
1227
|
+
this.notifyRichUpdate(viewPath, frame.key, previousValue, merged, 'patch', frame.data);
|
|
1228
|
+
break;
|
|
1229
|
+
}
|
|
1230
|
+
case 'delete':
|
|
1231
|
+
viewData.delete(frame.key);
|
|
1232
|
+
this.notifyUpdate(viewPath, frame.key, {
|
|
1233
|
+
type: 'delete',
|
|
1234
|
+
key: frame.key,
|
|
1235
|
+
});
|
|
1236
|
+
if (previousValue !== undefined) {
|
|
1237
|
+
this.notifyRichDelete(viewPath, frame.key, previousValue);
|
|
1238
|
+
}
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
getAll(viewPath) {
|
|
1243
|
+
const viewData = this.views.get(viewPath);
|
|
1244
|
+
if (!viewData)
|
|
1245
|
+
return [];
|
|
1246
|
+
return viewData.values();
|
|
1247
|
+
}
|
|
1248
|
+
get(viewPath, key) {
|
|
1249
|
+
const viewData = this.views.get(viewPath);
|
|
1250
|
+
if (!viewData)
|
|
1251
|
+
return null;
|
|
1252
|
+
const value = viewData.get(key);
|
|
1253
|
+
return value !== undefined ? value : null;
|
|
1254
|
+
}
|
|
1255
|
+
getAllSync(viewPath) {
|
|
1256
|
+
const viewData = this.views.get(viewPath);
|
|
1257
|
+
if (!viewData)
|
|
1258
|
+
return undefined;
|
|
1259
|
+
return viewData.values();
|
|
1260
|
+
}
|
|
1261
|
+
getSync(viewPath, key) {
|
|
1262
|
+
const viewData = this.views.get(viewPath);
|
|
1263
|
+
if (!viewData)
|
|
1264
|
+
return undefined;
|
|
1265
|
+
const value = viewData.get(key);
|
|
1266
|
+
return value !== undefined ? value : null;
|
|
1267
|
+
}
|
|
1268
|
+
keys(viewPath) {
|
|
1269
|
+
const viewData = this.views.get(viewPath);
|
|
1270
|
+
if (!viewData)
|
|
1271
|
+
return [];
|
|
1272
|
+
return viewData.keys();
|
|
1273
|
+
}
|
|
1274
|
+
size(viewPath) {
|
|
1275
|
+
const viewData = this.views.get(viewPath);
|
|
1276
|
+
return viewData?.size ?? 0;
|
|
1277
|
+
}
|
|
1278
|
+
clear() {
|
|
1279
|
+
this.views.clear();
|
|
1280
|
+
}
|
|
1281
|
+
clearView(viewPath) {
|
|
1282
|
+
this.views.delete(viewPath);
|
|
1283
|
+
this.viewConfigs.delete(viewPath);
|
|
1284
|
+
}
|
|
1285
|
+
getViewConfig(viewPath) {
|
|
1286
|
+
return this.viewConfigs.get(viewPath);
|
|
1287
|
+
}
|
|
1288
|
+
setViewConfig(viewPath, config) {
|
|
1289
|
+
this.viewConfigs.set(viewPath, config);
|
|
1290
|
+
const existingView = this.views.get(viewPath);
|
|
1291
|
+
if (existingView && config.sort) {
|
|
1292
|
+
existingView.setSortConfig(config.sort);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
onUpdate(callback) {
|
|
1296
|
+
this.updateCallbacks.add(callback);
|
|
1297
|
+
return () => {
|
|
1298
|
+
this.updateCallbacks.delete(callback);
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
onRichUpdate(callback) {
|
|
1302
|
+
this.richUpdateCallbacks.add(callback);
|
|
1303
|
+
return () => {
|
|
1304
|
+
this.richUpdateCallbacks.delete(callback);
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
subscribe(viewPath, callback) {
|
|
1308
|
+
const handler = (path, _key, update) => {
|
|
1309
|
+
if (path === viewPath) {
|
|
1310
|
+
callback(update);
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
this.updateCallbacks.add(handler);
|
|
1314
|
+
return () => {
|
|
1315
|
+
this.updateCallbacks.delete(handler);
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
subscribeToKey(viewPath, key, callback) {
|
|
1319
|
+
const handler = (path, updateKey, update) => {
|
|
1320
|
+
if (path === viewPath && updateKey === key) {
|
|
1321
|
+
callback(update);
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
this.updateCallbacks.add(handler);
|
|
1325
|
+
return () => {
|
|
1326
|
+
this.updateCallbacks.delete(handler);
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
notifyUpdate(viewPath, key, update) {
|
|
1330
|
+
for (const callback of this.updateCallbacks) {
|
|
1331
|
+
callback(viewPath, key, update);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
notifyRichUpdate(viewPath, key, before, after, _op, patch) {
|
|
1335
|
+
const richUpdate = before === undefined
|
|
1336
|
+
? { type: 'created', key, data: after }
|
|
1337
|
+
: { type: 'updated', key, before, after, patch };
|
|
1338
|
+
for (const callback of this.richUpdateCallbacks) {
|
|
1339
|
+
callback(viewPath, key, richUpdate);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
notifyRichDelete(viewPath, key, lastKnown) {
|
|
1343
|
+
const richUpdate = { type: 'deleted', key, lastKnown };
|
|
1344
|
+
for (const callback of this.richUpdateCallbacks) {
|
|
1345
|
+
callback(viewPath, key, richUpdate);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
846
1350
|
exports.ConnectionManager = ConnectionManager;
|
|
847
1351
|
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
|
848
1352
|
exports.DEFAULT_MAX_ENTRIES_PER_VIEW = DEFAULT_MAX_ENTRIES_PER_VIEW;
|
|
1353
|
+
exports.EntityStore = EntityStore;
|
|
849
1354
|
exports.FrameProcessor = FrameProcessor;
|
|
850
1355
|
exports.HyperStack = HyperStack;
|
|
851
1356
|
exports.HyperStackError = HyperStackError;
|
|
@@ -856,7 +1361,9 @@ exports.createTypedListView = createTypedListView;
|
|
|
856
1361
|
exports.createTypedStateView = createTypedStateView;
|
|
857
1362
|
exports.createTypedViews = createTypedViews;
|
|
858
1363
|
exports.createUpdateStream = createUpdateStream;
|
|
1364
|
+
exports.isEntityFrame = isEntityFrame;
|
|
859
1365
|
exports.isSnapshotFrame = isSnapshotFrame;
|
|
1366
|
+
exports.isSubscribedFrame = isSubscribedFrame;
|
|
860
1367
|
exports.isValidFrame = isValidFrame;
|
|
861
1368
|
exports.parseFrame = parseFrame;
|
|
862
1369
|
exports.parseFrameFromBlob = parseFrameFromBlob;
|