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.esm.js
CHANGED
|
@@ -15,40 +15,32 @@ class HyperStackError extends Error {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
typeof obj.data === 'string');
|
|
23
|
-
}
|
|
24
|
-
function decompressGzip(base64Data) {
|
|
25
|
-
const binaryString = atob(base64Data);
|
|
26
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
27
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
28
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
29
|
-
}
|
|
30
|
-
const decompressed = inflate(bytes);
|
|
31
|
-
return new TextDecoder().decode(decompressed);
|
|
32
|
-
}
|
|
33
|
-
function parseAndDecompress(jsonString) {
|
|
34
|
-
const parsed = JSON.parse(jsonString);
|
|
35
|
-
if (isCompressedFrame(parsed)) {
|
|
36
|
-
const decompressedJson = decompressGzip(parsed.data);
|
|
37
|
-
const frame = JSON.parse(decompressedJson);
|
|
38
|
-
return frame;
|
|
39
|
-
}
|
|
40
|
-
return parsed;
|
|
18
|
+
const GZIP_MAGIC_0 = 0x1f;
|
|
19
|
+
const GZIP_MAGIC_1 = 0x8b;
|
|
20
|
+
function isGzipData(data) {
|
|
21
|
+
return data.length >= 2 && data[0] === GZIP_MAGIC_0 && data[1] === GZIP_MAGIC_1;
|
|
41
22
|
}
|
|
42
23
|
function isSnapshotFrame(frame) {
|
|
43
24
|
return frame.op === 'snapshot';
|
|
44
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
|
+
}
|
|
45
32
|
function parseFrame(data) {
|
|
46
33
|
if (typeof data === 'string') {
|
|
47
|
-
return
|
|
34
|
+
return JSON.parse(data);
|
|
35
|
+
}
|
|
36
|
+
const bytes = new Uint8Array(data);
|
|
37
|
+
if (isGzipData(bytes)) {
|
|
38
|
+
const decompressed = inflate(bytes);
|
|
39
|
+
const jsonString = new TextDecoder().decode(decompressed);
|
|
40
|
+
return JSON.parse(jsonString);
|
|
48
41
|
}
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
return parseAndDecompress(jsonString);
|
|
42
|
+
const jsonString = new TextDecoder('utf-8').decode(data);
|
|
43
|
+
return JSON.parse(jsonString);
|
|
52
44
|
}
|
|
53
45
|
async function parseFrameFromBlob(blob) {
|
|
54
46
|
const arrayBuffer = await blob.arrayBuffer();
|
|
@@ -123,8 +115,8 @@ class ConnectionManager {
|
|
|
123
115
|
this.reconnectAttempts = 0;
|
|
124
116
|
this.updateState('connected');
|
|
125
117
|
this.startPingInterval();
|
|
126
|
-
this.flushSubscriptionQueue();
|
|
127
118
|
this.resubscribeActive();
|
|
119
|
+
this.flushSubscriptionQueue();
|
|
128
120
|
resolve();
|
|
129
121
|
};
|
|
130
122
|
this.ws.onmessage = async (event) => {
|
|
@@ -145,7 +137,6 @@ class ConnectionManager {
|
|
|
145
137
|
this.notifyFrameHandlers(frame);
|
|
146
138
|
}
|
|
147
139
|
catch (error) {
|
|
148
|
-
console.error('[hyperstack] Error parsing frame:', error);
|
|
149
140
|
this.updateState('error', 'Failed to parse frame from server');
|
|
150
141
|
}
|
|
151
142
|
};
|
|
@@ -183,12 +174,18 @@ class ConnectionManager {
|
|
|
183
174
|
subscribe(subscription) {
|
|
184
175
|
const subKey = this.makeSubKey(subscription);
|
|
185
176
|
if (this.currentState === 'connected' && this.ws?.readyState === WebSocket.OPEN) {
|
|
177
|
+
if (this.activeSubscriptions.has(subKey)) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
186
180
|
const subMsg = { type: 'subscribe', ...subscription };
|
|
187
181
|
this.ws.send(JSON.stringify(subMsg));
|
|
188
182
|
this.activeSubscriptions.add(subKey);
|
|
189
183
|
}
|
|
190
184
|
else {
|
|
191
|
-
this.subscriptionQueue.
|
|
185
|
+
const alreadyQueued = this.subscriptionQueue.some((s) => this.makeSubKey(s) === subKey);
|
|
186
|
+
if (!alreadyQueued) {
|
|
187
|
+
this.subscriptionQueue.push(subscription);
|
|
188
|
+
}
|
|
192
189
|
}
|
|
193
190
|
}
|
|
194
191
|
unsubscribe(view, key) {
|
|
@@ -278,11 +275,11 @@ class ConnectionManager {
|
|
|
278
275
|
}
|
|
279
276
|
}
|
|
280
277
|
|
|
281
|
-
function isObject(item) {
|
|
278
|
+
function isObject$1(item) {
|
|
282
279
|
return item !== null && typeof item === 'object' && !Array.isArray(item);
|
|
283
280
|
}
|
|
284
|
-
function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
285
|
-
if (!isObject(target) || !isObject(source)) {
|
|
281
|
+
function deepMergeWithAppend$1(target, source, appendPaths, currentPath = '') {
|
|
282
|
+
if (!isObject$1(target) || !isObject$1(source)) {
|
|
286
283
|
return source;
|
|
287
284
|
}
|
|
288
285
|
const result = { ...target };
|
|
@@ -298,8 +295,8 @@ function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
|
298
295
|
result[key] = sourceValue;
|
|
299
296
|
}
|
|
300
297
|
}
|
|
301
|
-
else if (isObject(sourceValue) && isObject(targetValue)) {
|
|
302
|
-
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);
|
|
303
300
|
}
|
|
304
301
|
else {
|
|
305
302
|
result[key] = sourceValue;
|
|
@@ -309,20 +306,107 @@ function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
|
309
306
|
}
|
|
310
307
|
class FrameProcessor {
|
|
311
308
|
constructor(storage, config = {}) {
|
|
309
|
+
this.pendingUpdates = [];
|
|
310
|
+
this.flushTimer = null;
|
|
311
|
+
this.isProcessing = false;
|
|
312
312
|
this.storage = storage;
|
|
313
313
|
this.maxEntriesPerView = config.maxEntriesPerView === undefined
|
|
314
314
|
? DEFAULT_MAX_ENTRIES_PER_VIEW
|
|
315
315
|
: config.maxEntriesPerView;
|
|
316
|
+
this.flushIntervalMs = config.flushIntervalMs ?? 0;
|
|
316
317
|
}
|
|
317
318
|
handleFrame(frame) {
|
|
318
|
-
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)) {
|
|
319
380
|
this.handleSnapshotFrame(frame);
|
|
320
381
|
}
|
|
321
382
|
else {
|
|
322
383
|
this.handleEntityFrame(frame);
|
|
323
384
|
}
|
|
324
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
|
+
}
|
|
325
405
|
handleSnapshotFrame(frame) {
|
|
406
|
+
this.handleSnapshotFrameWithoutEnforce(frame);
|
|
407
|
+
this.enforceMaxEntries(frame.entity);
|
|
408
|
+
}
|
|
409
|
+
handleSnapshotFrameWithoutEnforce(frame) {
|
|
326
410
|
const viewPath = frame.entity;
|
|
327
411
|
for (const entity of frame.data) {
|
|
328
412
|
const previousValue = this.storage.get(viewPath, entity.key);
|
|
@@ -334,16 +418,18 @@ class FrameProcessor {
|
|
|
334
418
|
});
|
|
335
419
|
this.emitRichUpdate(viewPath, entity.key, previousValue, entity.data, 'upsert');
|
|
336
420
|
}
|
|
337
|
-
this.enforceMaxEntries(viewPath);
|
|
338
421
|
}
|
|
339
422
|
handleEntityFrame(frame) {
|
|
423
|
+
this.handleEntityFrameWithoutEnforce(frame);
|
|
424
|
+
this.enforceMaxEntries(frame.entity);
|
|
425
|
+
}
|
|
426
|
+
handleEntityFrameWithoutEnforce(frame) {
|
|
340
427
|
const viewPath = frame.entity;
|
|
341
428
|
const previousValue = this.storage.get(viewPath, frame.key);
|
|
342
429
|
switch (frame.op) {
|
|
343
430
|
case 'create':
|
|
344
431
|
case 'upsert':
|
|
345
432
|
this.storage.set(viewPath, frame.key, frame.data);
|
|
346
|
-
this.enforceMaxEntries(viewPath);
|
|
347
433
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
348
434
|
type: 'upsert',
|
|
349
435
|
key: frame.key,
|
|
@@ -355,10 +441,9 @@ class FrameProcessor {
|
|
|
355
441
|
const existing = this.storage.get(viewPath, frame.key);
|
|
356
442
|
const appendPaths = frame.append ?? [];
|
|
357
443
|
const merged = existing
|
|
358
|
-
? deepMergeWithAppend(existing, frame.data, appendPaths)
|
|
444
|
+
? deepMergeWithAppend$1(existing, frame.data, appendPaths)
|
|
359
445
|
: frame.data;
|
|
360
446
|
this.storage.set(viewPath, frame.key, merged);
|
|
361
|
-
this.enforceMaxEntries(viewPath);
|
|
362
447
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
363
448
|
type: 'patch',
|
|
364
449
|
key: frame.key,
|
|
@@ -397,7 +482,7 @@ class FrameProcessor {
|
|
|
397
482
|
}
|
|
398
483
|
}
|
|
399
484
|
|
|
400
|
-
class ViewData {
|
|
485
|
+
let ViewData$1 = class ViewData {
|
|
401
486
|
constructor() {
|
|
402
487
|
this.entities = new Map();
|
|
403
488
|
this.accessOrder = [];
|
|
@@ -451,7 +536,7 @@ class ViewData {
|
|
|
451
536
|
this.entities.clear();
|
|
452
537
|
this.accessOrder = [];
|
|
453
538
|
}
|
|
454
|
-
}
|
|
539
|
+
};
|
|
455
540
|
class MemoryAdapter {
|
|
456
541
|
constructor(_config = {}) {
|
|
457
542
|
this.views = new Map();
|
|
@@ -499,7 +584,7 @@ class MemoryAdapter {
|
|
|
499
584
|
set(viewPath, key, data) {
|
|
500
585
|
let view = this.views.get(viewPath);
|
|
501
586
|
if (!view) {
|
|
502
|
-
view = new ViewData();
|
|
587
|
+
view = new ViewData$1();
|
|
503
588
|
this.views.set(viewPath, view);
|
|
504
589
|
}
|
|
505
590
|
view.set(key, data);
|
|
@@ -840,5 +925,425 @@ class HyperStack {
|
|
|
840
925
|
}
|
|
841
926
|
}
|
|
842
927
|
|
|
843
|
-
|
|
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 };
|
|
844
1349
|
//# sourceMappingURL=index.esm.js.map
|