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.js CHANGED
@@ -17,40 +17,32 @@ class HyperStackError extends Error {
17
17
  }
18
18
  }
19
19
 
20
- function isCompressedFrame(obj) {
21
- return (typeof obj === 'object' &&
22
- obj !== null &&
23
- obj.compressed === 'gzip' &&
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 parseAndDecompress(data);
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 decoder = new TextDecoder('utf-8');
52
- const jsonString = decoder.decode(data);
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.push(subscription);
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 (isSnapshotFrame(frame)) {
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;