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