hyperstack-typescript 0.3.1 → 0.3.3
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 +97 -7
- package/dist/index.esm.js +549 -44
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +551 -43
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -17,40 +17,32 @@ class HyperStackError extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
typeof obj.data === 'string');
|
|
25
|
-
}
|
|
26
|
-
function decompressGzip(base64Data) {
|
|
27
|
-
const binaryString = atob(base64Data);
|
|
28
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
29
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
30
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
31
|
-
}
|
|
32
|
-
const decompressed = pako.inflate(bytes);
|
|
33
|
-
return new TextDecoder().decode(decompressed);
|
|
34
|
-
}
|
|
35
|
-
function parseAndDecompress(jsonString) {
|
|
36
|
-
const parsed = JSON.parse(jsonString);
|
|
37
|
-
if (isCompressedFrame(parsed)) {
|
|
38
|
-
const decompressedJson = decompressGzip(parsed.data);
|
|
39
|
-
const frame = JSON.parse(decompressedJson);
|
|
40
|
-
return frame;
|
|
41
|
-
}
|
|
42
|
-
return parsed;
|
|
20
|
+
const GZIP_MAGIC_0 = 0x1f;
|
|
21
|
+
const GZIP_MAGIC_1 = 0x8b;
|
|
22
|
+
function isGzipData(data) {
|
|
23
|
+
return data.length >= 2 && data[0] === GZIP_MAGIC_0 && data[1] === GZIP_MAGIC_1;
|
|
43
24
|
}
|
|
44
25
|
function isSnapshotFrame(frame) {
|
|
45
26
|
return frame.op === 'snapshot';
|
|
46
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
|
+
}
|
|
47
34
|
function parseFrame(data) {
|
|
48
35
|
if (typeof data === 'string') {
|
|
49
|
-
return
|
|
36
|
+
return JSON.parse(data);
|
|
37
|
+
}
|
|
38
|
+
const bytes = new Uint8Array(data);
|
|
39
|
+
if (isGzipData(bytes)) {
|
|
40
|
+
const decompressed = pako.inflate(bytes);
|
|
41
|
+
const jsonString = new TextDecoder().decode(decompressed);
|
|
42
|
+
return JSON.parse(jsonString);
|
|
50
43
|
}
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
return parseAndDecompress(jsonString);
|
|
44
|
+
const jsonString = new TextDecoder('utf-8').decode(data);
|
|
45
|
+
return JSON.parse(jsonString);
|
|
54
46
|
}
|
|
55
47
|
async function parseFrameFromBlob(blob) {
|
|
56
48
|
const arrayBuffer = await blob.arrayBuffer();
|
|
@@ -125,8 +117,8 @@ class ConnectionManager {
|
|
|
125
117
|
this.reconnectAttempts = 0;
|
|
126
118
|
this.updateState('connected');
|
|
127
119
|
this.startPingInterval();
|
|
128
|
-
this.flushSubscriptionQueue();
|
|
129
120
|
this.resubscribeActive();
|
|
121
|
+
this.flushSubscriptionQueue();
|
|
130
122
|
resolve();
|
|
131
123
|
};
|
|
132
124
|
this.ws.onmessage = async (event) => {
|
|
@@ -147,7 +139,6 @@ class ConnectionManager {
|
|
|
147
139
|
this.notifyFrameHandlers(frame);
|
|
148
140
|
}
|
|
149
141
|
catch (error) {
|
|
150
|
-
console.error('[hyperstack] Error parsing frame:', error);
|
|
151
142
|
this.updateState('error', 'Failed to parse frame from server');
|
|
152
143
|
}
|
|
153
144
|
};
|
|
@@ -185,12 +176,18 @@ class ConnectionManager {
|
|
|
185
176
|
subscribe(subscription) {
|
|
186
177
|
const subKey = this.makeSubKey(subscription);
|
|
187
178
|
if (this.currentState === 'connected' && this.ws?.readyState === WebSocket.OPEN) {
|
|
179
|
+
if (this.activeSubscriptions.has(subKey)) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
188
182
|
const subMsg = { type: 'subscribe', ...subscription };
|
|
189
183
|
this.ws.send(JSON.stringify(subMsg));
|
|
190
184
|
this.activeSubscriptions.add(subKey);
|
|
191
185
|
}
|
|
192
186
|
else {
|
|
193
|
-
this.subscriptionQueue.
|
|
187
|
+
const alreadyQueued = this.subscriptionQueue.some((s) => this.makeSubKey(s) === subKey);
|
|
188
|
+
if (!alreadyQueued) {
|
|
189
|
+
this.subscriptionQueue.push(subscription);
|
|
190
|
+
}
|
|
194
191
|
}
|
|
195
192
|
}
|
|
196
193
|
unsubscribe(view, key) {
|
|
@@ -280,11 +277,11 @@ class ConnectionManager {
|
|
|
280
277
|
}
|
|
281
278
|
}
|
|
282
279
|
|
|
283
|
-
function isObject(item) {
|
|
280
|
+
function isObject$1(item) {
|
|
284
281
|
return item !== null && typeof item === 'object' && !Array.isArray(item);
|
|
285
282
|
}
|
|
286
|
-
function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
287
|
-
if (!isObject(target) || !isObject(source)) {
|
|
283
|
+
function deepMergeWithAppend$1(target, source, appendPaths, currentPath = '') {
|
|
284
|
+
if (!isObject$1(target) || !isObject$1(source)) {
|
|
288
285
|
return source;
|
|
289
286
|
}
|
|
290
287
|
const result = { ...target };
|
|
@@ -300,8 +297,8 @@ function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
|
300
297
|
result[key] = sourceValue;
|
|
301
298
|
}
|
|
302
299
|
}
|
|
303
|
-
else if (isObject(sourceValue) && isObject(targetValue)) {
|
|
304
|
-
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);
|
|
305
302
|
}
|
|
306
303
|
else {
|
|
307
304
|
result[key] = sourceValue;
|
|
@@ -311,20 +308,107 @@ function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
|
311
308
|
}
|
|
312
309
|
class FrameProcessor {
|
|
313
310
|
constructor(storage, config = {}) {
|
|
311
|
+
this.pendingUpdates = [];
|
|
312
|
+
this.flushTimer = null;
|
|
313
|
+
this.isProcessing = false;
|
|
314
314
|
this.storage = storage;
|
|
315
315
|
this.maxEntriesPerView = config.maxEntriesPerView === undefined
|
|
316
316
|
? DEFAULT_MAX_ENTRIES_PER_VIEW
|
|
317
317
|
: config.maxEntriesPerView;
|
|
318
|
+
this.flushIntervalMs = config.flushIntervalMs ?? 0;
|
|
318
319
|
}
|
|
319
320
|
handleFrame(frame) {
|
|
320
|
-
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)) {
|
|
321
382
|
this.handleSnapshotFrame(frame);
|
|
322
383
|
}
|
|
323
384
|
else {
|
|
324
385
|
this.handleEntityFrame(frame);
|
|
325
386
|
}
|
|
326
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
|
+
}
|
|
327
407
|
handleSnapshotFrame(frame) {
|
|
408
|
+
this.handleSnapshotFrameWithoutEnforce(frame);
|
|
409
|
+
this.enforceMaxEntries(frame.entity);
|
|
410
|
+
}
|
|
411
|
+
handleSnapshotFrameWithoutEnforce(frame) {
|
|
328
412
|
const viewPath = frame.entity;
|
|
329
413
|
for (const entity of frame.data) {
|
|
330
414
|
const previousValue = this.storage.get(viewPath, entity.key);
|
|
@@ -336,16 +420,18 @@ class FrameProcessor {
|
|
|
336
420
|
});
|
|
337
421
|
this.emitRichUpdate(viewPath, entity.key, previousValue, entity.data, 'upsert');
|
|
338
422
|
}
|
|
339
|
-
this.enforceMaxEntries(viewPath);
|
|
340
423
|
}
|
|
341
424
|
handleEntityFrame(frame) {
|
|
425
|
+
this.handleEntityFrameWithoutEnforce(frame);
|
|
426
|
+
this.enforceMaxEntries(frame.entity);
|
|
427
|
+
}
|
|
428
|
+
handleEntityFrameWithoutEnforce(frame) {
|
|
342
429
|
const viewPath = frame.entity;
|
|
343
430
|
const previousValue = this.storage.get(viewPath, frame.key);
|
|
344
431
|
switch (frame.op) {
|
|
345
432
|
case 'create':
|
|
346
433
|
case 'upsert':
|
|
347
434
|
this.storage.set(viewPath, frame.key, frame.data);
|
|
348
|
-
this.enforceMaxEntries(viewPath);
|
|
349
435
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
350
436
|
type: 'upsert',
|
|
351
437
|
key: frame.key,
|
|
@@ -357,10 +443,9 @@ class FrameProcessor {
|
|
|
357
443
|
const existing = this.storage.get(viewPath, frame.key);
|
|
358
444
|
const appendPaths = frame.append ?? [];
|
|
359
445
|
const merged = existing
|
|
360
|
-
? deepMergeWithAppend(existing, frame.data, appendPaths)
|
|
446
|
+
? deepMergeWithAppend$1(existing, frame.data, appendPaths)
|
|
361
447
|
: frame.data;
|
|
362
448
|
this.storage.set(viewPath, frame.key, merged);
|
|
363
|
-
this.enforceMaxEntries(viewPath);
|
|
364
449
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
365
450
|
type: 'patch',
|
|
366
451
|
key: frame.key,
|
|
@@ -399,7 +484,7 @@ class FrameProcessor {
|
|
|
399
484
|
}
|
|
400
485
|
}
|
|
401
486
|
|
|
402
|
-
class ViewData {
|
|
487
|
+
let ViewData$1 = class ViewData {
|
|
403
488
|
constructor() {
|
|
404
489
|
this.entities = new Map();
|
|
405
490
|
this.accessOrder = [];
|
|
@@ -453,7 +538,7 @@ class ViewData {
|
|
|
453
538
|
this.entities.clear();
|
|
454
539
|
this.accessOrder = [];
|
|
455
540
|
}
|
|
456
|
-
}
|
|
541
|
+
};
|
|
457
542
|
class MemoryAdapter {
|
|
458
543
|
constructor(_config = {}) {
|
|
459
544
|
this.views = new Map();
|
|
@@ -501,7 +586,7 @@ class MemoryAdapter {
|
|
|
501
586
|
set(viewPath, key, data) {
|
|
502
587
|
let view = this.views.get(viewPath);
|
|
503
588
|
if (!view) {
|
|
504
|
-
view = new ViewData();
|
|
589
|
+
view = new ViewData$1();
|
|
505
590
|
this.views.set(viewPath, view);
|
|
506
591
|
}
|
|
507
592
|
view.set(key, data);
|
|
@@ -842,9 +927,430 @@ class HyperStack {
|
|
|
842
927
|
}
|
|
843
928
|
}
|
|
844
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
|
+
|
|
845
1350
|
exports.ConnectionManager = ConnectionManager;
|
|
846
1351
|
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
|
847
1352
|
exports.DEFAULT_MAX_ENTRIES_PER_VIEW = DEFAULT_MAX_ENTRIES_PER_VIEW;
|
|
1353
|
+
exports.EntityStore = EntityStore;
|
|
848
1354
|
exports.FrameProcessor = FrameProcessor;
|
|
849
1355
|
exports.HyperStack = HyperStack;
|
|
850
1356
|
exports.HyperStackError = HyperStackError;
|
|
@@ -855,7 +1361,9 @@ exports.createTypedListView = createTypedListView;
|
|
|
855
1361
|
exports.createTypedStateView = createTypedStateView;
|
|
856
1362
|
exports.createTypedViews = createTypedViews;
|
|
857
1363
|
exports.createUpdateStream = createUpdateStream;
|
|
1364
|
+
exports.isEntityFrame = isEntityFrame;
|
|
858
1365
|
exports.isSnapshotFrame = isSnapshotFrame;
|
|
1366
|
+
exports.isSubscribedFrame = isSubscribedFrame;
|
|
859
1367
|
exports.isValidFrame = isValidFrame;
|
|
860
1368
|
exports.parseFrame = parseFrame;
|
|
861
1369
|
exports.parseFrameFromBlob = parseFrameFromBlob;
|