@usearete/sdk 0.25.0 → 0.25.1

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/README.md CHANGED
@@ -382,8 +382,11 @@ for await (const update of session.stacks.myStack.views.settlementGame.list.watc
382
382
  }
383
383
 
384
384
  const game = await session.stacks.myStack.views.settlementGame.state.get('game-123');
385
+ const latest = await session.stacks.ore.views.OreRound.latest.getOne();
385
386
  ```
386
387
 
388
+ `get` and `getOne` open (or reuse) an equivalent subscription, wait for its initial snapshot, and release it. They reject with `InitialDataTimeoutError` after `timeoutMs` (5000 by default; `null` waits forever). `getSync` only reads a subscription that is already active and returns `undefined` when there is none.
389
+
387
390
  Every options object is a protocol v2 query with independent ordered membership. Different windows and filters on the same view can run concurrently, while equivalent normalized queries share one reference-counted wire subscription:
388
391
 
389
392
  ```ts
package/dist/index.cjs CHANGED
@@ -444,6 +444,7 @@ class SubscriptionRegistry {
444
444
  this.queryStore = queryStore;
445
445
  this.subscriptions = new Map();
446
446
  this.subscriptionsById = new Map();
447
+ this.failureListeners = new Set();
447
448
  }
448
449
  subscribe(query, snapshotEnabled = true) {
449
450
  const request = normalizeSubscriptionRequest({
@@ -506,12 +507,30 @@ class SubscriptionRegistry {
506
507
  ? this.queryStore.getSnapshot(tracker.subscription.subscriptionId)
507
508
  : undefined;
508
509
  }
509
- handleConnectionState(state) {
510
+ handleConnectionState(state, message) {
510
511
  if (state === 'reconnecting')
511
512
  this.queryStore.beginReconnect();
512
- if (state === 'error') {
513
- this.queryStore.failRefreshing(new AreteError('Connection failed while refreshing subscriptions', 'CONNECTION_ERROR'));
513
+ if (state !== 'error') {
514
+ this.connectionError = undefined;
515
+ return;
514
516
  }
517
+ this.queryStore.failRefreshing(new AreteError('Connection failed while refreshing subscriptions', 'CONNECTION_ERROR'));
518
+ // Queries still waiting for their first snapshot are not failed above;
519
+ // one-shot reads learn about the failure here instead of timing out.
520
+ this.connectionError = new AreteError(message ?? 'Connection failed', 'CONNECTION_ERROR');
521
+ this.notifyFailure(this.connectionError);
522
+ }
523
+ /** The terminal connection failure while the connection is in the `error` state. */
524
+ getConnectionError() {
525
+ return this.connectionError;
526
+ }
527
+ /**
528
+ * Called when pending queries can no longer resolve: the connection failed
529
+ * terminally, or a disconnect cleared every subscription.
530
+ */
531
+ onFailure(callback) {
532
+ this.failureListeners.add(callback);
533
+ return () => this.failureListeners.delete(callback);
515
534
  }
516
535
  clear() {
517
536
  for (const { subscription } of this.subscriptions.values()) {
@@ -527,6 +546,11 @@ class SubscriptionRegistry {
527
546
  }
528
547
  this.subscriptions.clear();
529
548
  this.subscriptionsById.clear();
549
+ this.notifyFailure(new AreteError('Subscriptions were cleared by a disconnect', 'CONNECTION_CANCELLED'));
550
+ }
551
+ notifyFailure(error) {
552
+ for (const listener of [...this.failureListeners])
553
+ listener(error);
530
554
  }
531
555
  createLease(tracker) {
532
556
  let released = false;
@@ -3565,10 +3589,90 @@ function createRichUpdateStream(_storage, subscriptionRegistry, query, keyFilter
3565
3589
  };
3566
3590
  }
3567
3591
 
3592
+ /** How long `get` and `getOne` wait for an initial snapshot, as in the Rust and Python SDKs. */
3593
+ const DEFAULT_INITIAL_DATA_TIMEOUT_MS = 5000;
3594
+ /**
3595
+ * `get` or `getOne` gave up waiting for the initial snapshot: the socket never
3596
+ * delivered it (no connection, a reconnect storm, a server that never
3597
+ * answers). The read's subscription is released before this is thrown.
3598
+ */
3599
+ class InitialDataTimeoutError extends AreteError {
3600
+ constructor(view, timeoutMs) {
3601
+ super(`Timed out after ${timeoutMs}ms waiting for the initial snapshot of view '${view}'`, 'INITIAL_DATA_TIMEOUT', { view, timeoutMs });
3602
+ this.view = view;
3603
+ this.timeoutMs = timeoutMs;
3604
+ this.name = 'InitialDataTimeoutError';
3605
+ }
3606
+ }
3568
3607
  function queryOptions(options) {
3569
- const { schema: _schema, withSnapshot, ...query } = options ?? {};
3608
+ const { schema: _schema, withSnapshot, timeoutMs: _timeoutMs, ...query } = options ?? {};
3570
3609
  return { query, snapshotEnabled: withSnapshot ?? true };
3571
3610
  }
3611
+ function resolveTimeout(options) {
3612
+ const timeoutMs = options?.timeoutMs === undefined
3613
+ ? DEFAULT_INITIAL_DATA_TIMEOUT_MS
3614
+ : options.timeoutMs;
3615
+ if (timeoutMs !== null && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
3616
+ throw new RangeError('timeoutMs must be a positive finite number or null');
3617
+ }
3618
+ return timeoutMs;
3619
+ }
3620
+ /**
3621
+ * Open (or reuse) the equivalent subscription, wait until its snapshot has
3622
+ * resolved or its query has failed, and release it. A terminal connection
3623
+ * failure or a disconnect rejects with that reason. Bounded by `timeoutMs`
3624
+ * (`null` waits forever) so a socket that never delivers cannot hang the caller.
3625
+ */
3626
+ async function readResolved(registry, query, snapshotEnabled, timeoutMs) {
3627
+ const lease = registry.subscribe(query, snapshotEnabled);
3628
+ try {
3629
+ await new Promise((resolve, reject) => {
3630
+ let settled = false;
3631
+ let stop = () => { };
3632
+ let timer;
3633
+ const finish = (error) => {
3634
+ if (settled)
3635
+ return;
3636
+ settled = true;
3637
+ stop();
3638
+ if (timer !== undefined)
3639
+ clearTimeout(timer);
3640
+ if (error)
3641
+ reject(error);
3642
+ else
3643
+ resolve();
3644
+ };
3645
+ const check = () => {
3646
+ const error = lease.getError();
3647
+ if (error)
3648
+ finish(error);
3649
+ else if (!lease.getSnapshot().isLoading)
3650
+ finish();
3651
+ else {
3652
+ const connectionError = registry.getConnectionError();
3653
+ if (connectionError)
3654
+ finish(connectionError);
3655
+ }
3656
+ };
3657
+ check();
3658
+ if (settled)
3659
+ return;
3660
+ const stopChanges = lease.onChange(check);
3661
+ const stopFailures = registry.onFailure(finish);
3662
+ stop = () => {
3663
+ stopChanges();
3664
+ stopFailures();
3665
+ };
3666
+ if (timeoutMs !== null) {
3667
+ timer = setTimeout(() => finish(new InitialDataTimeoutError(query.view, timeoutMs)), timeoutMs);
3668
+ }
3669
+ });
3670
+ return lease.getSnapshot();
3671
+ }
3672
+ finally {
3673
+ lease.release();
3674
+ }
3675
+ }
3572
3676
  function serializeViewKeyValue(value, view, field) {
3573
3677
  const location = field === undefined ? `view '${view}'` : `key field '${field}' for view '${view}'`;
3574
3678
  if (typeof value === 'string')
@@ -3619,7 +3723,10 @@ function createTypedStateView(viewDef, storage, subscriptionRegistry) {
3619
3723
  return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key: serializedKey, ...query }, serializedKey, snapshotEnabled);
3620
3724
  },
3621
3725
  async get(key, options) {
3622
- return this.getSync(key, options) ?? null;
3726
+ const timeoutMs = resolveTimeout(options);
3727
+ const { query, snapshotEnabled } = queryOptions(options);
3728
+ const snapshot = await readResolved(subscriptionRegistry, { view: viewDef.view, key: wireKey(key), ...query }, snapshotEnabled, timeoutMs);
3729
+ return snapshot.data[0] ?? null;
3623
3730
  },
3624
3731
  getSync(key, options) {
3625
3732
  const { query, snapshotEnabled } = queryOptions(options);
@@ -3647,7 +3754,14 @@ function createTypedListView(viewDef, storage, subscriptionRegistry) {
3647
3754
  return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...query }, undefined, snapshotEnabled);
3648
3755
  },
3649
3756
  async get(options) {
3650
- return this.getSync(options) ?? [];
3757
+ const timeoutMs = resolveTimeout(options);
3758
+ const { query, snapshotEnabled } = queryOptions(options);
3759
+ const snapshot = await readResolved(subscriptionRegistry, { view: viewDef.view, ...query }, snapshotEnabled, timeoutMs);
3760
+ return [...snapshot.data];
3761
+ },
3762
+ async getOne(options) {
3763
+ const items = await this.get({ ...options, take: 1 });
3764
+ return items[0] ?? null;
3651
3765
  },
3652
3766
  getSync(options) {
3653
3767
  const { query, snapshotEnabled } = queryOptions(options);
@@ -7565,8 +7679,8 @@ let Arete$1 = class Arete {
7565
7679
  this.connection.onFrame((frame) => {
7566
7680
  this.processor.handleFrame(frame);
7567
7681
  });
7568
- this.connection.onStateChange((state) => {
7569
- this.subscriptionRegistry.handleConnectionState(state);
7682
+ this.connection.onStateChange((state, error) => {
7683
+ this.subscriptionRegistry.handleConnectionState(state, error);
7570
7684
  });
7571
7685
  this._views = createTypedViews(this.stack, this.storage, this.subscriptionRegistry);
7572
7686
  this._queries = this.buildQueries();
@@ -8554,9 +8668,11 @@ exports.Arete = Arete;
8554
8668
  exports.AreteError = AreteError;
8555
8669
  exports.ConnectionManager = ConnectionManager;
8556
8670
  exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
8671
+ exports.DEFAULT_INITIAL_DATA_TIMEOUT_MS = DEFAULT_INITIAL_DATA_TIMEOUT_MS;
8557
8672
  exports.DEFAULT_MAX_ENTRIES_PER_VIEW = DEFAULT_MAX_ENTRIES_PER_VIEW;
8558
8673
  exports.EXTENSION_API_VERSION = EXTENSION_API_VERSION;
8559
8674
  exports.FrameProcessor = FrameProcessor;
8675
+ exports.InitialDataTimeoutError = InitialDataTimeoutError;
8560
8676
  exports.InstructionError = InstructionError;
8561
8677
  exports.MemoryAdapter = MemoryAdapter;
8562
8678
  exports.OPERATION_KIND = OPERATION_KIND;