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