@usearete/sdk 0.2.0 → 0.3.0

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.esm.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { inflate } from 'pako';
2
- import { Point } from '@noble/ed25519';
2
+ import { Buffer } from 'buffer';
3
3
 
4
4
  const DEFAULT_MAX_ENTRIES_PER_VIEW = 10000;
5
5
  const DEFAULT_CONFIG = {
6
+ autoReconnect: true,
6
7
  reconnectIntervals: [1000, 2000, 4000, 8000, 16000],
7
8
  maxReconnectAttempts: 5,
8
9
  maxEntriesPerView: DEFAULT_MAX_ENTRIES_PER_VIEW,
@@ -67,51 +68,441 @@ function parseErrorCode(errorCode) {
67
68
 
68
69
  const GZIP_MAGIC_0 = 0x1f;
69
70
  const GZIP_MAGIC_1 = 0x8b;
71
+ const FRAME_MODES = new Set(['state', 'append', 'list']);
72
+ const LIVE_OPS = new Set(['upsert', 'patch', 'remove', 'delete']);
73
+ const QUERY_FIELDS$1 = new Set([
74
+ 'view',
75
+ 'key',
76
+ 'partition',
77
+ 'filters',
78
+ 'take',
79
+ 'skip',
80
+ 'after',
81
+ 'snapshotLimit',
82
+ ]);
83
+ function isRecord(value) {
84
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
85
+ }
70
86
  function isGzipData(data) {
71
87
  return data.length >= 2 && data[0] === GZIP_MAGIC_0 && data[1] === GZIP_MAGIC_1;
72
88
  }
89
+ function isSubscriptionId(value) {
90
+ if (typeof value !== 'string' || value.length === 0 || value.trim() !== value)
91
+ return false;
92
+ if (/\p{Cc}/u.test(value))
93
+ return false;
94
+ return new TextEncoder().encode(value).byteLength <= 128;
95
+ }
96
+ function isMode(value) {
97
+ return typeof value === 'string' && FRAME_MODES.has(value);
98
+ }
99
+ function isSort(value) {
100
+ if (!isRecord(value))
101
+ return false;
102
+ return Array.isArray(value['field'])
103
+ && value['field'].every((entry) => typeof entry === 'string')
104
+ && (value['order'] === 'asc' || value['order'] === 'desc');
105
+ }
106
+ function isQuery(value) {
107
+ if (!isRecord(value) || typeof value['view'] !== 'string' || value['view'].length === 0) {
108
+ return false;
109
+ }
110
+ if (Object.keys(value).some((key) => !QUERY_FIELDS$1.has(key)))
111
+ return false;
112
+ if (value['key'] !== undefined && typeof value['key'] !== 'string')
113
+ return false;
114
+ if (value['partition'] !== undefined && typeof value['partition'] !== 'string')
115
+ return false;
116
+ if (value['filters'] !== undefined && !isRecord(value['filters']))
117
+ return false;
118
+ if (value['take'] !== undefined && (!Number.isInteger(value['take']) || Number(value['take']) <= 0)) {
119
+ return false;
120
+ }
121
+ if (value['skip'] !== undefined && (!Number.isInteger(value['skip']) || Number(value['skip']) < 0)) {
122
+ return false;
123
+ }
124
+ if (value['after'] !== undefined && typeof value['after'] !== 'string')
125
+ return false;
126
+ return value['snapshotLimit'] === undefined
127
+ || (Number.isInteger(value['snapshotLimit']) && Number(value['snapshotLimit']) > 0);
128
+ }
129
+ function hasV2Identity(value) {
130
+ return value['protocolVersion'] === 2 && isSubscriptionId(value['subscriptionId']);
131
+ }
73
132
  function isSnapshotFrame(frame) {
74
- return frame.op === 'snapshot';
133
+ return 'op' in frame && frame.op === 'snapshot';
75
134
  }
76
135
  function isSubscribedFrame(frame) {
77
- return frame.op === 'subscribed';
136
+ return 'op' in frame && frame.op === 'subscribed';
137
+ }
138
+ function isUnsubscribedFrame(frame) {
139
+ return 'op' in frame && frame.op === 'unsubscribed';
140
+ }
141
+ function isErrorFrame(frame) {
142
+ return 'type' in frame && frame.type === 'error';
78
143
  }
79
144
  function isEntityFrame(frame) {
80
- return ['create', 'upsert', 'patch', 'delete'].includes(frame.op);
145
+ return 'op' in frame && LIVE_OPS.has(frame.op);
81
146
  }
82
- function parseFrame(data) {
83
- if (typeof data === 'string') {
147
+ function decodeFrame(data) {
148
+ if (typeof data === 'string')
84
149
  return JSON.parse(data);
85
- }
86
150
  const bytes = new Uint8Array(data);
87
- if (isGzipData(bytes)) {
88
- const decompressed = inflate(bytes);
89
- const jsonString = new TextDecoder().decode(decompressed);
90
- return JSON.parse(jsonString);
151
+ const decoded = isGzipData(bytes) ? inflate(bytes) : bytes;
152
+ return JSON.parse(new TextDecoder('utf-8').decode(decoded));
153
+ }
154
+ function parseFrame(data) {
155
+ const frame = decodeFrame(data);
156
+ if (!isValidFrame(frame)) {
157
+ throw new Error('Invalid WebSocket protocol v2 frame');
91
158
  }
92
- const jsonString = new TextDecoder('utf-8').decode(data);
93
- return JSON.parse(jsonString);
159
+ return frame;
94
160
  }
95
161
  async function parseFrameFromBlob(blob) {
96
- const arrayBuffer = await blob.arrayBuffer();
97
- return parseFrame(arrayBuffer);
162
+ return parseFrame(await blob.arrayBuffer());
98
163
  }
99
164
  function isValidFrame(frame) {
100
- if (typeof frame !== 'object' || frame === null) {
165
+ if (!isRecord(frame) || frame['protocolVersion'] !== 2)
166
+ return false;
167
+ if (frame['type'] === 'error') {
168
+ return (frame['subscriptionId'] === null || isSubscriptionId(frame['subscriptionId']))
169
+ && typeof frame['code'] === 'string'
170
+ && typeof frame['fatal'] === 'boolean'
171
+ && (frame['message'] === undefined || typeof frame['message'] === 'string')
172
+ && (frame['error'] === undefined || typeof frame['error'] === 'string')
173
+ && (frame['retryable'] === undefined || typeof frame['retryable'] === 'boolean');
174
+ }
175
+ if (!hasV2Identity(frame) || typeof frame['op'] !== 'string')
101
176
  return false;
177
+ if (frame['op'] === 'unsubscribed')
178
+ return true;
179
+ if (frame['op'] === 'subscribed') {
180
+ return isQuery(frame['query'])
181
+ && isMode(frame['mode'])
182
+ && (frame['sort'] === undefined || isSort(frame['sort']));
102
183
  }
103
- const f = frame;
104
- if (typeof f['entity'] !== 'string' ||
105
- typeof f['op'] !== 'string' ||
106
- typeof f['mode'] !== 'string' ||
107
- !['state', 'append', 'list'].includes(f['mode'])) {
184
+ if (!isMode(frame['mode']) || typeof frame['entity'] !== 'string')
108
185
  return false;
186
+ if (frame['op'] === 'snapshot') {
187
+ return typeof frame['snapshotId'] === 'string'
188
+ && frame['snapshotId'].length > 0
189
+ && typeof frame['authoritative'] === 'boolean'
190
+ && typeof frame['complete'] === 'boolean'
191
+ && (frame['key'] === undefined || typeof frame['key'] === 'string')
192
+ && Array.isArray(frame['data'])
193
+ && frame['data'].every((entry) => isRecord(entry) && typeof entry['key'] === 'string' && 'data' in entry);
194
+ }
195
+ return LIVE_OPS.has(frame['op'])
196
+ && typeof frame['key'] === 'string'
197
+ && 'data' in frame
198
+ && (frame['seq'] === undefined || typeof frame['seq'] === 'string')
199
+ && (frame['append'] === undefined
200
+ || (Array.isArray(frame['append'])
201
+ && frame['append'].every((entry) => typeof entry === 'string')));
202
+ }
203
+
204
+ let fallbackId = 0;
205
+ const QUERY_FIELDS = new Set([
206
+ 'view',
207
+ 'key',
208
+ 'partition',
209
+ 'filters',
210
+ 'take',
211
+ 'skip',
212
+ 'after',
213
+ 'snapshotLimit',
214
+ ]);
215
+ const SUBSCRIPTION_FIELDS = new Set([
216
+ 'type',
217
+ 'protocolVersion',
218
+ 'subscriptionId',
219
+ 'query',
220
+ 'snapshot',
221
+ ]);
222
+ function canonicalJsonValue(value, path) {
223
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
224
+ return value;
225
+ if (typeof value === 'number' && Number.isFinite(value))
226
+ return value;
227
+ if (Array.isArray(value)) {
228
+ return value.map((entry, index) => canonicalJsonValue(entry, `${path}[${index}]`));
229
+ }
230
+ if (typeof value === 'object') {
231
+ return Object.fromEntries(Object.entries(value)
232
+ .sort(([left], [right]) => left.localeCompare(right))
233
+ .map(([key, entry]) => [key, canonicalJsonValue(entry, `${path}.${key}`)]));
109
234
  }
110
- if (f['op'] === 'snapshot') {
111
- return Array.isArray(f['data']);
235
+ throw new TypeError(`${path} must contain JSON values`);
236
+ }
237
+ function assertPositiveInteger(value, field) {
238
+ if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
239
+ throw new TypeError(`${field} must be a positive integer`);
240
+ }
241
+ }
242
+ function validateSubscriptionId(subscriptionId) {
243
+ if (subscriptionId.length === 0 || subscriptionId.trim() !== subscriptionId) {
244
+ throw new TypeError('subscriptionId must be non-empty with no surrounding whitespace');
245
+ }
246
+ if (/\p{Cc}/u.test(subscriptionId)) {
247
+ throw new TypeError('subscriptionId must not contain control characters');
248
+ }
249
+ if (new TextEncoder().encode(subscriptionId).byteLength > 128) {
250
+ throw new TypeError('subscriptionId must not exceed 128 bytes');
251
+ }
252
+ }
253
+ function createSubscriptionId() {
254
+ const randomUuid = globalThis.crypto?.randomUUID?.();
255
+ if (randomUuid)
256
+ return `a4-${randomUuid}`;
257
+ fallbackId++;
258
+ return `a4-${Date.now().toString(36)}-${fallbackId.toString(36)}`;
259
+ }
260
+ function normalizeSubscriptionQuery(query) {
261
+ if (typeof query.view !== 'string' || query.view.length === 0) {
262
+ throw new TypeError('query.view must be a non-empty string');
263
+ }
264
+ if (Object.keys(query).some((key) => !QUERY_FIELDS.has(key))) {
265
+ throw new TypeError('query contains an unknown protocol v2 field');
266
+ }
267
+ if (query.key !== undefined && typeof query.key !== 'string') {
268
+ throw new TypeError('query.key must be a string');
269
+ }
270
+ if (query.partition !== undefined && typeof query.partition !== 'string') {
271
+ throw new TypeError('query.partition must be a string');
272
+ }
273
+ if (query.filters !== undefined
274
+ && (query.filters === null || typeof query.filters !== 'object' || Array.isArray(query.filters))) {
275
+ throw new TypeError('query.filters must be an object');
276
+ }
277
+ if (query.after !== undefined && typeof query.after !== 'string') {
278
+ throw new TypeError('query.after must be a string');
279
+ }
280
+ assertPositiveInteger(query.take, 'query.take');
281
+ assertPositiveInteger(query.snapshotLimit, 'query.snapshotLimit');
282
+ if (query.skip !== undefined && (!Number.isInteger(query.skip) || query.skip < 0)) {
283
+ throw new TypeError('query.skip must be a non-negative integer');
284
+ }
285
+ const normalized = { view: query.view };
286
+ if (query.key !== undefined)
287
+ normalized.key = query.key;
288
+ if (query.partition !== undefined)
289
+ normalized.partition = query.partition;
290
+ if (query.filters !== undefined) {
291
+ normalized.filters = Object.fromEntries(Object.entries(query.filters)
292
+ .sort(([left], [right]) => left.localeCompare(right))
293
+ .map(([path, value]) => [path, canonicalJsonValue(value, `query.filters.${path}`)]));
294
+ }
295
+ if (query.take !== undefined)
296
+ normalized.take = query.take;
297
+ if (query.skip !== undefined)
298
+ normalized.skip = query.skip;
299
+ if (query.after !== undefined)
300
+ normalized.after = query.after;
301
+ if (query.snapshotLimit !== undefined)
302
+ normalized.snapshotLimit = query.snapshotLimit;
303
+ return normalized;
304
+ }
305
+ function normalizeSubscriptionRequest(request) {
306
+ const enabled = request.snapshot?.enabled ?? true;
307
+ if (typeof enabled !== 'boolean') {
308
+ throw new TypeError('snapshot.enabled must be a boolean');
309
+ }
310
+ return {
311
+ query: normalizeSubscriptionQuery(request.query),
312
+ snapshot: { enabled },
313
+ };
314
+ }
315
+ function canonicalQueryKey(request) {
316
+ const normalized = normalizeSubscriptionRequest(request);
317
+ return JSON.stringify({ query: normalized.query, snapshot: normalized.snapshot });
318
+ }
319
+ function normalizeSubscription(subscription) {
320
+ if (subscription.type !== 'subscribe' || subscription.protocolVersion !== 2) {
321
+ throw new TypeError('Subscription must use WebSocket protocolVersion 2');
322
+ }
323
+ if (Object.keys(subscription).some((key) => !SUBSCRIPTION_FIELDS.has(key))) {
324
+ throw new TypeError('Subscription contains an unknown protocol v2 field');
325
+ }
326
+ validateSubscriptionId(subscription.subscriptionId);
327
+ const request = normalizeSubscriptionRequest(subscription);
328
+ return {
329
+ type: 'subscribe',
330
+ protocolVersion: 2,
331
+ subscriptionId: subscription.subscriptionId,
332
+ query: request.query,
333
+ snapshot: request.snapshot,
334
+ };
335
+ }
336
+ function refreshError(value) {
337
+ if (value instanceof AreteError)
338
+ return value;
339
+ return new AreteError(value instanceof Error ? value.message : 'Subscription refresh failed', 'SUBSCRIPTION_ERROR', value);
340
+ }
341
+ class SubscriptionRegistry {
342
+ constructor(connection, queryStore) {
343
+ this.connection = connection;
344
+ this.queryStore = queryStore;
345
+ this.subscriptions = new Map();
346
+ this.subscriptionsById = new Map();
347
+ }
348
+ subscribe(query, snapshotEnabled = true) {
349
+ const request = normalizeSubscriptionRequest({
350
+ query,
351
+ snapshot: { enabled: snapshotEnabled },
352
+ });
353
+ const queryKey = canonicalQueryKey(request);
354
+ let tracker = this.subscriptions.get(queryKey);
355
+ if (tracker) {
356
+ tracker.refCount++;
357
+ }
358
+ else {
359
+ const subscription = {
360
+ type: 'subscribe',
361
+ protocolVersion: 2,
362
+ subscriptionId: createSubscriptionId(),
363
+ query: request.query,
364
+ snapshot: request.snapshot,
365
+ };
366
+ tracker = { subscription, queryKey, refCount: 1 };
367
+ this.queryStore.register(subscription, queryKey);
368
+ try {
369
+ this.connection.subscribe(subscription);
370
+ }
371
+ catch (error) {
372
+ this.queryStore.unregister(subscription.subscriptionId);
373
+ throw error;
374
+ }
375
+ this.subscriptions.set(queryKey, tracker);
376
+ this.subscriptionsById.set(subscription.subscriptionId, tracker);
377
+ }
378
+ return this.createLease(tracker);
379
+ }
380
+ refresh(query, snapshotEnabled = true) {
381
+ const tracker = this.getTracker(query, snapshotEnabled);
382
+ if (!tracker) {
383
+ return Promise.reject(new AreteError(`Cannot refresh inactive query '${query.view}'`, 'SUBSCRIPTION_NOT_FOUND'));
384
+ }
385
+ return this.refreshTracker(tracker);
386
+ }
387
+ /**
388
+ * Refresh every active subscription for a view, optionally narrowed to one
389
+ * serialized key. Resolves immediately when nothing matches — refreshing a
390
+ * view with no active subscriptions is a no-op, not an error.
391
+ */
392
+ refreshView(view, key) {
393
+ const matches = Array.from(this.subscriptions.values()).filter(({ subscription }) => subscription.query.view === view
394
+ && (key === undefined || subscription.query.key === key));
395
+ return Promise.all(matches.map((tracker) => this.refreshTracker(tracker))).then(() => undefined);
396
+ }
397
+ getRefCount(query, snapshotEnabled = true) {
398
+ return this.getTracker(query, snapshotEnabled)?.refCount ?? 0;
399
+ }
400
+ getActiveSubscriptions() {
401
+ return Array.from(this.subscriptions.values(), ({ subscription }) => normalizeSubscription(subscription));
402
+ }
403
+ getSnapshot(query, snapshotEnabled = true) {
404
+ const tracker = this.getTracker(query, snapshotEnabled);
405
+ return tracker
406
+ ? this.queryStore.getSnapshot(tracker.subscription.subscriptionId)
407
+ : undefined;
408
+ }
409
+ handleConnectionState(state) {
410
+ if (state === 'reconnecting')
411
+ this.queryStore.beginReconnect();
412
+ if (state === 'error') {
413
+ this.queryStore.failRefreshing(new AreteError('Connection failed while refreshing subscriptions', 'CONNECTION_ERROR'));
414
+ }
415
+ }
416
+ clear() {
417
+ for (const { subscription } of this.subscriptions.values()) {
418
+ try {
419
+ this.connection.unsubscribe(subscription.subscriptionId);
420
+ }
421
+ catch {
422
+ // Local release must complete even when the socket can no longer send.
423
+ }
424
+ finally {
425
+ this.queryStore.unregister(subscription.subscriptionId);
426
+ }
427
+ }
428
+ this.subscriptions.clear();
429
+ this.subscriptionsById.clear();
430
+ }
431
+ createLease(tracker) {
432
+ let released = false;
433
+ const subscriptionId = tracker.subscription.subscriptionId;
434
+ return {
435
+ subscription: tracker.subscription,
436
+ queryKey: tracker.queryKey,
437
+ getSnapshot: () => {
438
+ const snapshot = this.queryStore.getSnapshot(subscriptionId);
439
+ if (!snapshot) {
440
+ throw new AreteError('Query lease has been released', 'SUBSCRIPTION_NOT_FOUND');
441
+ }
442
+ return snapshot;
443
+ },
444
+ onChange: (callback) => this.queryStore.onChange(subscriptionId, callback),
445
+ onUpdate: (callback) => this.queryStore.onUpdate(subscriptionId, callback),
446
+ onRichUpdate: (callback) => this.queryStore.onRichUpdate(subscriptionId, callback),
447
+ refresh: () => this.refreshTracker(tracker),
448
+ release: () => {
449
+ if (released)
450
+ return;
451
+ released = true;
452
+ this.releaseTracker(tracker);
453
+ },
454
+ };
455
+ }
456
+ getTracker(query, snapshotEnabled) {
457
+ return this.subscriptions.get(canonicalQueryKey({
458
+ query,
459
+ snapshot: { enabled: snapshotEnabled },
460
+ }));
461
+ }
462
+ refreshTracker(tracker) {
463
+ if (this.subscriptionsById.get(tracker.subscription.subscriptionId) !== tracker) {
464
+ return Promise.reject(new AreteError('Cannot refresh a released query lease', 'SUBSCRIPTION_NOT_FOUND'));
465
+ }
466
+ if (tracker.refreshPromise)
467
+ return tracker.refreshPromise;
468
+ const subscriptionId = tracker.subscription.subscriptionId;
469
+ const waitForSnapshot = tracker.subscription.snapshot.enabled;
470
+ const completion = this.queryStore.beginRefresh(subscriptionId, waitForSnapshot);
471
+ try {
472
+ this.connection.refresh(tracker.subscription);
473
+ }
474
+ catch (value) {
475
+ const error = refreshError(value);
476
+ this.queryStore.failRefresh(subscriptionId, error);
477
+ return waitForSnapshot ? completion : Promise.reject(error);
478
+ }
479
+ const promise = completion;
480
+ tracker.refreshPromise = promise;
481
+ const clear = () => {
482
+ if (tracker.refreshPromise === promise)
483
+ tracker.refreshPromise = undefined;
484
+ };
485
+ void promise.then(clear, clear);
486
+ return promise;
487
+ }
488
+ releaseTracker(tracker) {
489
+ if (this.subscriptionsById.get(tracker.subscription.subscriptionId) !== tracker)
490
+ return;
491
+ tracker.refCount--;
492
+ if (tracker.refCount > 0)
493
+ return;
494
+ this.subscriptions.delete(tracker.queryKey);
495
+ this.subscriptionsById.delete(tracker.subscription.subscriptionId);
496
+ try {
497
+ this.connection.unsubscribe(tracker.subscription.subscriptionId);
498
+ }
499
+ catch {
500
+ // ConnectionManager already removed local membership before sending.
501
+ }
502
+ finally {
503
+ this.queryStore.unregister(tracker.subscription.subscriptionId);
504
+ }
112
505
  }
113
- return (typeof f['key'] === 'string' &&
114
- ['create', 'upsert', 'patch', 'delete'].includes(f['op']));
115
506
  }
116
507
 
117
508
  const TOKEN_REFRESH_BUFFER_SECONDS = 60;
@@ -173,14 +564,15 @@ function isSocketIssueMessage(value) {
173
564
  }
174
565
  const candidate = value;
175
566
  return candidate['type'] === 'error'
176
- && typeof candidate['message'] === 'string'
177
567
  && typeof candidate['code'] === 'string'
178
- && typeof candidate['retryable'] === 'boolean'
179
- && typeof candidate['fatal'] === 'boolean';
568
+ && typeof candidate['fatal'] === 'boolean'
569
+ && (candidate['message'] === undefined || typeof candidate['message'] === 'string')
570
+ && (candidate['error'] === undefined || typeof candidate['error'] === 'string')
571
+ && (candidate['retryable'] === undefined || typeof candidate['retryable'] === 'boolean');
180
572
  }
181
- function isHostedAreteWebsocketUrl(websocketUrl) {
573
+ function isHostedAreteEndpoint(url) {
182
574
  try {
183
- return new URL(websocketUrl).hostname.toLowerCase().endsWith(HOSTED_WEBSOCKET_SUFFIX);
575
+ return new URL(url).hostname.toLowerCase().endsWith(HOSTED_WEBSOCKET_SUFFIX);
184
576
  }
185
577
  catch {
186
578
  return false;
@@ -199,24 +591,32 @@ class ConnectionManager {
199
591
  this.pingInterval = null;
200
592
  this.tokenRefreshTimeout = null;
201
593
  this.tokenRefreshInFlight = null;
594
+ this.tokenRequestInFlight = null;
202
595
  this.currentState = 'disconnected';
203
- this.subscriptionQueue = [];
204
- this.activeSubscriptions = new Set();
596
+ this.subscriptionQueue = new Map();
597
+ this.activeSubscriptions = new Map();
598
+ this.socketGeneration = 0;
599
+ this.pendingConnect = null;
205
600
  this.frameHandlers = new Set();
206
601
  this.stateHandlers = new Set();
207
602
  this.socketIssueHandlers = new Set();
603
+ this.tokenScopes = new Set();
604
+ this.requestedScopes = new Set();
208
605
  this.reconnectForTokenRefresh = false;
209
606
  const websocketUrl = config.websocketUrl && config.websocketUrl !== DISABLED_WEBSOCKET_URL
210
607
  ? config.websocketUrl
211
608
  : null;
212
609
  this.websocketUrl = websocketUrl;
213
- this.hostedAreteUrl = websocketUrl !== null && isHostedAreteWebsocketUrl(websocketUrl);
610
+ this.hostedAreteUrl = websocketUrl !== null && isHostedAreteEndpoint(websocketUrl);
611
+ this.autoReconnect = config.autoReconnect ?? DEFAULT_CONFIG.autoReconnect;
214
612
  this.reconnectIntervals = config.reconnectIntervals ?? DEFAULT_CONFIG.reconnectIntervals;
215
613
  this.maxReconnectAttempts =
216
614
  config.maxReconnectAttempts ?? DEFAULT_CONFIG.maxReconnectAttempts;
217
615
  this.authConfig = config.auth;
218
616
  if (config.initialSubscriptions) {
219
- this.subscriptionQueue.push(...config.initialSubscriptions);
617
+ for (const subscription of config.initialSubscriptions) {
618
+ this.addQueuedSubscription(subscription);
619
+ }
220
620
  }
221
621
  }
222
622
  getTokenEndpoint() {
@@ -246,7 +646,7 @@ class ConnectionManager {
246
646
  const strategy = this.getAuthStrategy();
247
647
  return strategy.kind === 'token-provider' || strategy.kind === 'token-endpoint';
248
648
  }
249
- updateTokenState(result) {
649
+ updateTokenState(result, requestedScopes) {
250
650
  const normalized = normalizeTokenResult(result);
251
651
  if (!normalized.token) {
252
652
  throw new AreteError('Authentication provider returned an empty token', 'TOKEN_INVALID');
@@ -254,6 +654,7 @@ class ConnectionManager {
254
654
  this.currentToken = normalized.token;
255
655
  this.tokenExpiry = normalizeExpiryTimestamp(normalized.expiresAt, normalized.expires_at)
256
656
  ?? parseJwtExpiry(normalized.token);
657
+ this.tokenScopes = new Set(normalized.scopes ?? requestedScopes);
257
658
  if (this.isTokenExpired()) {
258
659
  throw new AreteError('Authentication token is expired', 'TOKEN_EXPIRED');
259
660
  }
@@ -262,23 +663,65 @@ class ConnectionManager {
262
663
  clearTokenState() {
263
664
  this.currentToken = undefined;
264
665
  this.tokenExpiry = undefined;
666
+ this.tokenScopes.clear();
667
+ }
668
+ tokenCovers(scopes) {
669
+ return scopes.every((scope) => this.tokenScopes.has(scope));
265
670
  }
266
- async getOrRefreshToken(forceRefresh = false) {
267
- if (!forceRefresh && this.currentToken && !this.isTokenExpired()) {
671
+ async getOrRefreshToken(forceRefresh = false, requiredScopes = ['read']) {
672
+ for (const scope of requiredScopes)
673
+ this.requestedScopes.add(scope);
674
+ if (!forceRefresh && this.currentToken && !this.isTokenExpired() && this.tokenCovers(requiredScopes)) {
268
675
  return this.currentToken;
269
676
  }
677
+ if (this.tokenRequestInFlight) {
678
+ await this.tokenRequestInFlight;
679
+ if (!this.currentToken) {
680
+ return undefined;
681
+ }
682
+ if (this.currentToken && !this.isTokenExpired() && this.tokenCovers(requiredScopes)) {
683
+ return this.currentToken;
684
+ }
685
+ throw new AreteError(`Authentication token was not granted required scopes: ${requiredScopes.join(', ')}`, 'AUTH_REQUIRED');
686
+ }
687
+ let request;
688
+ request = Promise.resolve().then(() => this.fetchAuthToken([...new Set([...this.tokenScopes, ...this.requestedScopes])], () => this.tokenRequestInFlight === request));
689
+ this.tokenRequestInFlight = request;
690
+ try {
691
+ const token = await request;
692
+ if (token && !this.tokenCovers(requiredScopes)) {
693
+ throw new AreteError(`Authentication token was not granted required scopes: ${requiredScopes.join(', ')}`, 'AUTH_REQUIRED');
694
+ }
695
+ return token;
696
+ }
697
+ finally {
698
+ if (this.tokenRequestInFlight === request) {
699
+ this.tokenRequestInFlight = null;
700
+ }
701
+ }
702
+ }
703
+ async fetchAuthToken(scopes, isCurrent) {
270
704
  const strategy = this.getAuthStrategy();
705
+ const requireCurrent = () => {
706
+ if (!isCurrent()) {
707
+ throw new AreteError('Authentication request was superseded', 'CONNECTION_CANCELLED');
708
+ }
709
+ };
271
710
  // For hosted Arete URLs, auth is required - fail early with clear message
272
711
  if (strategy.kind === 'none' && this.hostedAreteUrl) {
273
712
  throw new AreteError('Arete authentication required. Please provide auth.publishableKey to AreteProvider. ' +
274
713
  'Get your key from https://arete.run/dashboard', 'AUTH_REQUIRED');
275
714
  }
276
715
  switch (strategy.kind) {
277
- case 'static-token':
278
- return this.updateTokenState(strategy.token);
716
+ case 'static-token': {
717
+ requireCurrent();
718
+ return this.updateTokenState(strategy.token, scopes);
719
+ }
279
720
  case 'token-provider':
280
721
  try {
281
- return this.updateTokenState(await strategy.getToken());
722
+ const result = await strategy.getToken({ scopes });
723
+ requireCurrent();
724
+ return this.updateTokenState(result, scopes);
282
725
  }
283
726
  catch (error) {
284
727
  if (error instanceof AreteError) {
@@ -288,7 +731,9 @@ class ConnectionManager {
288
731
  }
289
732
  case 'token-endpoint':
290
733
  try {
291
- return this.updateTokenState(await this.fetchTokenFromEndpoint(strategy.endpoint));
734
+ const result = await this.fetchTokenFromEndpoint(strategy.endpoint, scopes);
735
+ requireCurrent();
736
+ return this.updateTokenState(result, scopes);
292
737
  }
293
738
  catch (error) {
294
739
  if (error instanceof AreteError) {
@@ -297,15 +742,17 @@ class ConnectionManager {
297
742
  throw new AreteError('Failed to fetch authentication token from endpoint', 'AUTH_REQUIRED', error);
298
743
  }
299
744
  case 'none':
745
+ requireCurrent();
300
746
  return undefined;
301
747
  }
302
748
  }
303
- createTokenEndpointRequestBody() {
749
+ createTokenEndpointRequestBody(scopes) {
304
750
  return {
305
751
  websocket_url: this.websocketUrl ?? '',
752
+ scopes,
306
753
  };
307
754
  }
308
- async fetchTokenFromEndpoint(tokenEndpoint) {
755
+ async fetchTokenFromEndpoint(tokenEndpoint, scopes = [...this.requestedScopes]) {
309
756
  const response = await fetch(tokenEndpoint, {
310
757
  method: 'POST',
311
758
  headers: {
@@ -316,7 +763,7 @@ class ConnectionManager {
316
763
  'Content-Type': 'application/json',
317
764
  },
318
765
  credentials: this.authConfig?.tokenEndpointCredentials,
319
- body: JSON.stringify(this.createTokenEndpointRequestBody()),
766
+ body: JSON.stringify(this.createTokenEndpointRequestBody(scopes)),
320
767
  });
321
768
  if (!response.ok) {
322
769
  const rawError = await response.text();
@@ -381,10 +828,11 @@ class ConnectionManager {
381
828
  if (this.tokenRefreshInFlight) {
382
829
  return this.tokenRefreshInFlight;
383
830
  }
384
- this.tokenRefreshInFlight = (async () => {
831
+ let refresh;
832
+ refresh = (async () => {
385
833
  const previousToken = this.currentToken;
386
834
  try {
387
- await this.getOrRefreshToken(true);
835
+ await this.getOrRefreshToken(true, [...this.tokenScopes]);
388
836
  if (previousToken &&
389
837
  this.currentToken &&
390
838
  this.currentToken !== previousToken &&
@@ -396,16 +844,23 @@ class ConnectionManager {
396
844
  this.rotateConnectionForTokenRefresh();
397
845
  }
398
846
  }
399
- this.scheduleTokenRefresh();
847
+ if (this.tokenRefreshInFlight === refresh) {
848
+ this.scheduleTokenRefresh();
849
+ }
400
850
  }
401
851
  catch {
402
- this.scheduleTokenRefresh();
852
+ if (this.tokenRefreshInFlight === refresh) {
853
+ this.scheduleTokenRefresh();
854
+ }
403
855
  }
404
856
  finally {
405
- this.tokenRefreshInFlight = null;
857
+ if (this.tokenRefreshInFlight === refresh) {
858
+ this.tokenRefreshInFlight = null;
859
+ }
406
860
  }
407
861
  })();
408
- return this.tokenRefreshInFlight;
862
+ this.tokenRefreshInFlight = refresh;
863
+ return refresh;
409
864
  }
410
865
  async sendInBandAuthRefresh(token) {
411
866
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
@@ -451,6 +906,11 @@ class ConnectionManager {
451
906
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN || this.reconnectForTokenRefresh) {
452
907
  return;
453
908
  }
909
+ if (!this.autoReconnect) {
910
+ this.updateState('error', 'Token refresh requires a new connection, but automatic reconnection is disabled');
911
+ this.ws.close(1000, 'token refresh');
912
+ return;
913
+ }
454
914
  this.reconnectForTokenRefresh = true;
455
915
  this.updateState('reconnecting');
456
916
  this.ws.close(1000, 'token refresh');
@@ -490,8 +950,14 @@ class ConnectionManager {
490
950
  getState() {
491
951
  return this.currentState;
492
952
  }
493
- async getHttpAuthToken(forceRefresh = false) {
494
- return this.getOrRefreshToken(forceRefresh);
953
+ async getHttpAuthToken(requiredScopesOrForce = ['read'], forceRefresh = false) {
954
+ const requiredScopes = typeof requiredScopesOrForce === 'boolean'
955
+ ? ['read']
956
+ : requiredScopesOrForce;
957
+ const force = typeof requiredScopesOrForce === 'boolean'
958
+ ? requiredScopesOrForce
959
+ : forceRefresh;
960
+ return this.getOrRefreshToken(force, requiredScopes);
495
961
  }
496
962
  clearHttpAuthToken() {
497
963
  this.clearTokenState();
@@ -516,49 +982,75 @@ class ConnectionManager {
516
982
  }
517
983
  notifySocketIssue(message) {
518
984
  const issue = {
519
- error: message.error,
520
- message: message.message,
985
+ error: message.error ?? message.code,
986
+ message: message.message ?? message.error ?? message.code,
521
987
  code: parseErrorCode(message.code),
522
- retryable: message.retryable,
988
+ retryable: message.retryable ?? false,
523
989
  retryAfter: message.retry_after,
524
990
  suggestedAction: message.suggested_action,
525
991
  docsUrl: message.docs_url,
526
992
  fatal: message.fatal,
993
+ subscriptionId: message.subscriptionId,
527
994
  };
528
995
  for (const handler of this.socketIssueHandlers) {
529
996
  handler(issue);
530
997
  }
531
998
  return issue;
532
999
  }
533
- async connect() {
1000
+ async connect(recovering = false) {
534
1001
  this.requireWebsocketUrl();
535
1002
  if (this.ws?.readyState === WebSocket.OPEN ||
536
1003
  this.ws?.readyState === WebSocket.CONNECTING ||
537
1004
  this.currentState === 'connecting') {
538
1005
  return;
539
1006
  }
540
- this.updateState('connecting');
1007
+ const generation = ++this.socketGeneration;
1008
+ const cancellation = new Promise((_resolve, reject) => {
1009
+ this.pendingConnect = { generation, reject };
1010
+ });
1011
+ this.updateState(recovering ? 'reconnecting' : 'connecting');
1012
+ if (generation !== this.socketGeneration) {
1013
+ return cancellation;
1014
+ }
541
1015
  let token;
542
1016
  try {
543
- token = await this.getOrRefreshToken();
1017
+ token = await Promise.race([
1018
+ this.getOrRefreshToken(false, ['read']),
1019
+ cancellation,
1020
+ ]);
544
1021
  }
545
1022
  catch (error) {
546
- this.updateState('error', error instanceof Error ? error.message : 'Failed to get token');
1023
+ if (this.pendingConnect?.generation === generation) {
1024
+ this.pendingConnect = null;
1025
+ }
1026
+ if (generation === this.socketGeneration) {
1027
+ this.updateState(recovering ? 'reconnecting' : 'error', error instanceof Error ? error.message : 'Failed to get token');
1028
+ }
547
1029
  throw error;
548
1030
  }
1031
+ if (generation !== this.socketGeneration) {
1032
+ throw new AreteError('WebSocket connection attempt was superseded', 'CONNECTION_CANCELLED');
1033
+ }
549
1034
  const wsUrl = this.buildAuthUrl(token);
550
- return new Promise((resolve, reject) => {
1035
+ const socketConnection = new Promise((resolve, reject) => {
551
1036
  let settled = false;
552
1037
  const finish = (fn) => {
553
1038
  if (settled) {
554
1039
  return;
555
1040
  }
556
1041
  settled = true;
1042
+ if (this.pendingConnect?.generation === generation) {
1043
+ this.pendingConnect = null;
1044
+ }
557
1045
  fn();
558
1046
  };
559
1047
  try {
560
- this.ws = this.createWebSocket(wsUrl, token);
561
- this.ws.onopen = () => {
1048
+ const socket = this.createWebSocket(wsUrl, token);
1049
+ this.ws = socket;
1050
+ const isCurrentSocket = () => this.ws === socket && this.socketGeneration === generation;
1051
+ socket.onopen = () => {
1052
+ if (!isCurrentSocket())
1053
+ return;
562
1054
  this.reconnectAttempts = 0;
563
1055
  this.updateState('connected');
564
1056
  this.startPingInterval();
@@ -567,7 +1059,9 @@ class ConnectionManager {
567
1059
  this.flushSubscriptionQueue();
568
1060
  finish(() => resolve());
569
1061
  };
570
- this.ws.onmessage = async (event) => {
1062
+ socket.onmessage = async (event) => {
1063
+ if (!isCurrentSocket())
1064
+ return;
571
1065
  try {
572
1066
  let frame;
573
1067
  if (event.data instanceof ArrayBuffer) {
@@ -583,7 +1077,17 @@ class ConnectionManager {
583
1077
  return;
584
1078
  }
585
1079
  if (isSocketIssueMessage(parsed)) {
586
- this.handleSocketIssueMessage(parsed);
1080
+ if (parsed.protocolVersion === 2) {
1081
+ const issueFrame = parseFrame(JSON.stringify(parsed));
1082
+ if (isCurrentSocket())
1083
+ this.notifyFrameHandlers(issueFrame);
1084
+ if (parsed.subscriptionId === null || parsed.fatal) {
1085
+ this.handleSocketIssueMessage(parsed);
1086
+ }
1087
+ }
1088
+ else {
1089
+ this.handleSocketIssueMessage(parsed);
1090
+ }
587
1091
  return;
588
1092
  }
589
1093
  frame = parseFrame(JSON.stringify(parsed));
@@ -591,21 +1095,28 @@ class ConnectionManager {
591
1095
  else {
592
1096
  throw new AreteError(`Unsupported message type: ${typeof event.data}`, 'PARSE_ERROR');
593
1097
  }
594
- this.notifyFrameHandlers(frame);
1098
+ if (isCurrentSocket())
1099
+ this.notifyFrameHandlers(frame);
595
1100
  }
596
1101
  catch {
1102
+ if (!isCurrentSocket())
1103
+ return;
597
1104
  this.updateState('error', 'Failed to parse frame from server');
598
1105
  }
599
1106
  };
600
- this.ws.onerror = () => {
1107
+ socket.onerror = () => {
1108
+ if (!isCurrentSocket())
1109
+ return;
601
1110
  const error = new AreteError('WebSocket connection error', 'CONNECTION_ERROR');
602
1111
  const wasConnecting = this.currentState === 'connecting';
603
- this.updateState('error', error.message);
1112
+ this.updateState(wasConnecting || !this.autoReconnect ? 'error' : 'reconnecting', error.message);
604
1113
  if (wasConnecting) {
605
1114
  finish(() => reject(error));
606
1115
  }
607
1116
  };
608
- this.ws.onclose = (event) => {
1117
+ socket.onclose = (event) => {
1118
+ if (!isCurrentSocket())
1119
+ return;
609
1120
  this.stopPingInterval();
610
1121
  this.clearTokenRefreshTimeout();
611
1122
  this.ws = null;
@@ -614,13 +1125,17 @@ class ConnectionManager {
614
1125
  ? `${event.code}: ${event.reason}`
615
1126
  : `code ${event.code}`;
616
1127
  const errorMessage = `WebSocket closed before open (${detail})`;
617
- this.updateState('error', errorMessage);
1128
+ this.updateState(recovering ? 'reconnecting' : 'error', errorMessage);
618
1129
  finish(() => reject(new AreteError(errorMessage, 'CONNECTION_ERROR')));
619
1130
  return;
620
1131
  }
621
1132
  if (this.reconnectForTokenRefresh) {
622
1133
  this.reconnectForTokenRefresh = false;
623
- void this.connect().catch(() => {
1134
+ if (!this.autoReconnect) {
1135
+ this.updateState('error', 'WebSocket closed for token refresh and automatic reconnection is disabled');
1136
+ return;
1137
+ }
1138
+ void this.connect(true).catch(() => {
624
1139
  this.handleReconnect();
625
1140
  });
626
1141
  return;
@@ -636,8 +1151,12 @@ class ConnectionManager {
636
1151
  : /expired|invalid|token/i.test(closeReason);
637
1152
  if (isAuthError) {
638
1153
  this.clearTokenState();
1154
+ if (!this.autoReconnect) {
1155
+ this.updateState('error', `Authentication refresh requires reconnection, but automatic reconnection is disabled: ${closeReason || event.code}`);
1156
+ return;
1157
+ }
639
1158
  // Try to reconnect immediately with a fresh token
640
- void this.connect().catch(() => {
1159
+ void this.connect(true).catch(() => {
641
1160
  this.handleReconnect();
642
1161
  });
643
1162
  return;
@@ -653,85 +1172,142 @@ class ConnectionManager {
653
1172
  }
654
1173
  }
655
1174
  if (this.currentState !== 'disconnected') {
656
- this.handleReconnect();
1175
+ if (this.autoReconnect) {
1176
+ this.handleReconnect();
1177
+ }
1178
+ else {
1179
+ const detail = event.reason
1180
+ ? `${event.code}: ${event.reason}`
1181
+ : `code ${event.code}`;
1182
+ this.updateState('error', `WebSocket closed (${detail}) and automatic reconnection is disabled`);
1183
+ }
657
1184
  }
658
1185
  };
659
1186
  }
660
1187
  catch (error) {
661
1188
  const hsError = new AreteError('Failed to create WebSocket connection', 'CONNECTION_ERROR', error);
662
- this.updateState('error', hsError.message);
663
- reject(hsError);
1189
+ this.updateState(recovering ? 'reconnecting' : 'error', hsError.message);
1190
+ finish(() => reject(hsError));
664
1191
  }
665
1192
  });
1193
+ return Promise.race([socketConnection, cancellation]);
666
1194
  }
667
1195
  disconnect() {
668
1196
  this.clearReconnectTimeout();
669
1197
  this.stopPingInterval();
670
1198
  this.clearTokenRefreshTimeout();
671
1199
  this.reconnectForTokenRefresh = false;
672
- this.updateState('disconnected');
1200
+ const pendingConnect = this.pendingConnect;
1201
+ this.pendingConnect = null;
1202
+ this.tokenRequestInFlight = null;
1203
+ this.tokenRefreshInFlight = null;
1204
+ this.socketGeneration++;
1205
+ pendingConnect?.reject(new AreteError('WebSocket connection attempt was cancelled', 'CONNECTION_CANCELLED'));
673
1206
  if (this.ws) {
674
- this.ws.close();
1207
+ const socket = this.ws;
675
1208
  this.ws = null;
1209
+ socket.close();
676
1210
  }
1211
+ this.updateState('disconnected');
677
1212
  }
678
1213
  subscribe(subscription) {
679
1214
  this.requireWebsocketUrl();
680
- const subKey = this.makeSubKey(subscription);
1215
+ const normalized = normalizeSubscription(subscription);
1216
+ const subscriptionId = normalized.subscriptionId;
1217
+ const existing = this.activeSubscriptions.get(subscriptionId)
1218
+ ?? this.subscriptionQueue.get(subscriptionId);
1219
+ if (existing) {
1220
+ if (JSON.stringify(existing) !== JSON.stringify(normalized)) {
1221
+ throw new AreteError(`subscriptionId '${subscriptionId}' is already registered locally`, 'DUPLICATE_SUBSCRIPTION_ID');
1222
+ }
1223
+ return;
1224
+ }
681
1225
  if (this.currentState === 'connected' && this.ws?.readyState === WebSocket.OPEN) {
682
- if (this.activeSubscriptions.has(subKey)) {
683
- return;
1226
+ try {
1227
+ this.ws.send(JSON.stringify(normalized));
684
1228
  }
685
- const subMsg = { type: 'subscribe', ...subscription };
686
- this.ws.send(JSON.stringify(subMsg));
687
- this.activeSubscriptions.add(subKey);
1229
+ catch (error) {
1230
+ try {
1231
+ const unsubMsg = {
1232
+ type: 'unsubscribe',
1233
+ protocolVersion: 2,
1234
+ subscriptionId,
1235
+ };
1236
+ this.ws.send(JSON.stringify(unsubMsg));
1237
+ }
1238
+ catch {
1239
+ // The original send error is more useful; cleanup is best-effort.
1240
+ }
1241
+ throw error;
1242
+ }
1243
+ this.activeSubscriptions.set(subscriptionId, normalized);
688
1244
  }
689
1245
  else {
690
- const alreadyQueued = this.subscriptionQueue.some((queuedSubscription) => this.makeSubKey(queuedSubscription) === subKey);
691
- if (!alreadyQueued) {
692
- this.subscriptionQueue.push(subscription);
693
- }
1246
+ this.subscriptionQueue.set(subscriptionId, normalized);
694
1247
  }
695
1248
  }
696
- unsubscribe(view, key) {
697
- const subscription = { view, key };
698
- const subKey = this.makeSubKey(subscription);
699
- if (this.activeSubscriptions.has(subKey)) {
700
- this.activeSubscriptions.delete(subKey);
1249
+ unsubscribe(subscriptionId) {
1250
+ this.subscriptionQueue.delete(subscriptionId);
1251
+ if (this.activeSubscriptions.has(subscriptionId)) {
1252
+ this.activeSubscriptions.delete(subscriptionId);
701
1253
  if (this.ws?.readyState === WebSocket.OPEN) {
702
- const unsubMsg = { type: 'unsubscribe', view, key };
1254
+ const unsubMsg = { type: 'unsubscribe', protocolVersion: 2, subscriptionId };
703
1255
  this.ws.send(JSON.stringify(unsubMsg));
704
1256
  }
705
1257
  }
706
1258
  }
1259
+ refresh(subscription) {
1260
+ this.requireWebsocketUrl();
1261
+ const normalized = normalizeSubscription(subscription);
1262
+ const subscriptionId = normalized.subscriptionId;
1263
+ const existing = this.activeSubscriptions.get(subscriptionId)
1264
+ ?? this.subscriptionQueue.get(subscriptionId);
1265
+ if (!existing) {
1266
+ throw new AreteError(`Cannot refresh inactive subscription '${subscriptionId}'`, 'SUBSCRIPTION_NOT_FOUND');
1267
+ }
1268
+ if (JSON.stringify(existing) !== JSON.stringify(normalized)) {
1269
+ throw new AreteError('Cannot change a subscription while refreshing it', 'INVALID_SUBSCRIPTION');
1270
+ }
1271
+ try {
1272
+ this.unsubscribe(subscriptionId);
1273
+ this.subscribe(existing);
1274
+ }
1275
+ catch (error) {
1276
+ if (!this.activeSubscriptions.has(subscriptionId)
1277
+ && !this.subscriptionQueue.has(subscriptionId)) {
1278
+ this.addQueuedSubscription(existing);
1279
+ }
1280
+ throw error;
1281
+ }
1282
+ }
707
1283
  isConnected() {
708
1284
  return this.currentState === 'connected' && this.ws?.readyState === WebSocket.OPEN;
709
1285
  }
710
- makeSubKey(subscription) {
711
- return `${subscription.view}:${subscription.key ?? '*'}:${subscription.partition ?? ''}`;
712
- }
713
1286
  flushSubscriptionQueue() {
714
- while (this.subscriptionQueue.length > 0) {
715
- const subscription = this.subscriptionQueue.shift();
716
- if (subscription) {
717
- this.subscribe(subscription);
718
- }
1287
+ const queuedSubscriptions = Array.from(this.subscriptionQueue.values());
1288
+ this.subscriptionQueue.clear();
1289
+ for (const subscription of queuedSubscriptions) {
1290
+ this.subscribe(subscription);
719
1291
  }
720
1292
  }
721
1293
  resubscribeActive() {
722
- for (const subKey of this.activeSubscriptions) {
723
- const [view, key, partition] = subKey.split(':');
724
- const subscription = {
725
- view: view ?? '',
726
- key: key === '*' ? undefined : key,
727
- partition: partition || undefined,
728
- };
1294
+ for (const subscription of this.activeSubscriptions.values()) {
729
1295
  if (this.ws?.readyState === WebSocket.OPEN) {
730
- const subMsg = { type: 'subscribe', ...subscription };
731
- this.ws.send(JSON.stringify(subMsg));
1296
+ this.ws.send(JSON.stringify(subscription));
732
1297
  }
733
1298
  }
734
1299
  }
1300
+ addQueuedSubscription(subscription) {
1301
+ const normalized = normalizeSubscription(subscription);
1302
+ const subscriptionId = normalized.subscriptionId;
1303
+ const existing = this.subscriptionQueue.get(subscriptionId);
1304
+ if (existing && JSON.stringify(existing) !== JSON.stringify(normalized)) {
1305
+ throw new AreteError(`subscriptionId '${subscriptionId}' is already queued`, 'DUPLICATE_SUBSCRIPTION_ID');
1306
+ }
1307
+ if (!existing) {
1308
+ this.subscriptionQueue.set(subscriptionId, normalized);
1309
+ }
1310
+ }
735
1311
  updateState(state, error) {
736
1312
  this.currentState = state;
737
1313
  for (const handler of this.stateHandlers) {
@@ -744,6 +1320,10 @@ class ConnectionManager {
744
1320
  }
745
1321
  }
746
1322
  handleReconnect() {
1323
+ if (!this.autoReconnect) {
1324
+ this.updateState('error', 'Automatic reconnection is disabled');
1325
+ return;
1326
+ }
747
1327
  if (this.reconnectAttempts >= this.maxReconnectAttempts) {
748
1328
  this.updateState('error', `Max reconnection attempts (${this.reconnectAttempts}) reached`);
749
1329
  return;
@@ -753,8 +1333,12 @@ class ConnectionManager {
753
1333
  const delay = this.reconnectIntervals[attemptIndex] ?? 1000;
754
1334
  this.reconnectAttempts++;
755
1335
  this.reconnectTimeout = setTimeout(() => {
756
- this.connect().catch(() => {
757
- /* retry handled by onclose */
1336
+ this.connect(true).catch(() => {
1337
+ // Once a socket exists, its close event owns the next retry. Token
1338
+ // acquisition and socket construction can fail before that point.
1339
+ if (this.ws === null && this.currentState !== 'disconnected') {
1340
+ this.handleReconnect();
1341
+ }
758
1342
  });
759
1343
  }, delay);
760
1344
  }
@@ -781,6 +1365,14 @@ class ConnectionManager {
781
1365
  }
782
1366
 
783
1367
  const INTERNAL_SEQ_FIELD = '__seq';
1368
+ class ProcessedSlotTimeoutError extends Error {
1369
+ constructor(targetSlot, processedSlot) {
1370
+ super(`Timed out waiting for Arete to process slot ${targetSlot}`);
1371
+ this.name = 'ProcessedSlotTimeoutError';
1372
+ this.targetSlot = targetSlot;
1373
+ this.processedSlot = processedSlot;
1374
+ }
1375
+ }
784
1376
  function isObject(item) {
785
1377
  return item !== null && typeof item === 'object' && !Array.isArray(item);
786
1378
  }
@@ -848,6 +1440,8 @@ class FrameProcessor {
848
1440
  this.pendingUpdates = [];
849
1441
  this.flushTimer = null;
850
1442
  this.isProcessing = false;
1443
+ this.latestProcessedSlot = null;
1444
+ this.processedSlotWaiters = new Set();
851
1445
  this.storage = storage;
852
1446
  this.maxEntriesPerView = config.maxEntriesPerView === undefined
853
1447
  ? DEFAULT_MAX_ENTRIES_PER_VIEW
@@ -855,6 +1449,9 @@ class FrameProcessor {
855
1449
  this.flushIntervalMs = config.flushIntervalMs ?? 0;
856
1450
  this.schemas = config.schemas;
857
1451
  this.patchSchemas = config.patchSchemas;
1452
+ this.queryStore = config.queryStore;
1453
+ this.warnOnValidationError = config.warnOnValidationError ?? true;
1454
+ this.onValidationError = config.onValidationError;
858
1455
  }
859
1456
  getSchema(viewPath, patch = false) {
860
1457
  const schemas = patch ? this.patchSchemas : this.schemas;
@@ -866,20 +1463,44 @@ class FrameProcessor {
866
1463
  const entityKey = entityName;
867
1464
  return schemas[entityKey] ?? null;
868
1465
  }
869
- normalizeEntity(viewPath, data, patch = false) {
870
- const schema = this.getSchema(viewPath, patch)
871
- ?? (!patch ? null : this.getSchema(viewPath));
872
- if (!schema)
1466
+ normalizeEntity(viewPath, data, context, patch = false) {
1467
+ const fullSchema = this.getSchema(viewPath);
1468
+ const patchSchema = this.getSchema(viewPath, true);
1469
+ const schemas = patch
1470
+ ? [patchSchema, fullSchema]
1471
+ : [fullSchema, patchSchema];
1472
+ let validationError;
1473
+ for (const schema of schemas) {
1474
+ if (!schema)
1475
+ continue;
1476
+ try {
1477
+ const result = schema.safeParse(data);
1478
+ if (result.success) {
1479
+ return stripUndefinedProperties(result.data);
1480
+ }
1481
+ validationError = result.error;
1482
+ }
1483
+ catch (error) {
1484
+ validationError = error;
1485
+ }
1486
+ }
1487
+ if (!fullSchema && !patchSchema)
873
1488
  return data;
874
- const result = schema.safeParse(data);
875
- if (!result.success) {
876
- console.warn('[Arete] Frame validation failed:', {
877
- view: viewPath,
878
- error: result.error,
879
- });
880
- return null;
1489
+ const diagnostic = {
1490
+ view: viewPath,
1491
+ ...context,
1492
+ error: validationError,
1493
+ };
1494
+ try {
1495
+ this.onValidationError?.(diagnostic);
1496
+ }
1497
+ catch (callbackError) {
1498
+ console.error('[Arete] Frame validation callback failed:', callbackError);
881
1499
  }
882
- return stripUndefinedProperties(result.data);
1500
+ if (this.warnOnValidationError) {
1501
+ console.warn('[Arete] Frame validation failed:', diagnostic);
1502
+ }
1503
+ return null;
883
1504
  }
884
1505
  hasSchema(viewPath) {
885
1506
  return this.getSchema(viewPath) !== null;
@@ -935,8 +1556,19 @@ class FrameProcessor {
935
1556
  const seq = data[INTERNAL_SEQ_FIELD];
936
1557
  return typeof seq === 'string' ? seq : undefined;
937
1558
  }
938
- attachInternalSeq(viewPath, data, seq) {
939
- if (!seq || !this.hasSchema(viewPath) || !isObject(data)) {
1559
+ compareSeq(left, right) {
1560
+ const [leftSlot, leftIndex = ''] = left.split(':', 2);
1561
+ const [rightSlot, rightIndex = ''] = right.split(':', 2);
1562
+ if (/^\d+$/.test(leftSlot ?? '') && /^\d+$/.test(rightSlot ?? '')) {
1563
+ const leftValue = BigInt(leftSlot);
1564
+ const rightValue = BigInt(rightSlot);
1565
+ if (leftValue !== rightValue)
1566
+ return leftValue < rightValue ? -1 : 1;
1567
+ }
1568
+ return leftIndex.localeCompare(rightIndex);
1569
+ }
1570
+ attachInternalSeq(_viewPath, data, seq) {
1571
+ if (!seq || !isObject(data)) {
940
1572
  return data;
941
1573
  }
942
1574
  Object.defineProperty(data, INTERNAL_SEQ_FIELD, {
@@ -966,6 +1598,45 @@ class FrameProcessor {
966
1598
  }
967
1599
  this.flushPendingUpdates();
968
1600
  }
1601
+ getProcessedSlot() {
1602
+ return this.latestProcessedSlot;
1603
+ }
1604
+ waitForProcessedSlot(slot, options = {}) {
1605
+ const targetSlot = this.normalizeSlot(slot);
1606
+ if (this.latestProcessedSlot !== null && this.latestProcessedSlot >= targetSlot) {
1607
+ return Promise.resolve(this.latestProcessedSlot);
1608
+ }
1609
+ if (options.signal?.aborted) {
1610
+ return Promise.reject(this.createAbortError());
1611
+ }
1612
+ if (options.timeoutMs !== undefined
1613
+ && (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 0)) {
1614
+ return Promise.reject(new RangeError('timeoutMs must be a non-negative finite number'));
1615
+ }
1616
+ return new Promise((resolve, reject) => {
1617
+ const waiter = {
1618
+ targetSlot,
1619
+ resolve,
1620
+ reject,
1621
+ timeout: null,
1622
+ signal: options.signal,
1623
+ };
1624
+ const rejectWaiter = (error) => {
1625
+ this.removeProcessedSlotWaiter(waiter);
1626
+ reject(error);
1627
+ };
1628
+ if (options.timeoutMs !== undefined) {
1629
+ waiter.timeout = setTimeout(() => {
1630
+ rejectWaiter(new ProcessedSlotTimeoutError(targetSlot, this.latestProcessedSlot));
1631
+ }, options.timeoutMs);
1632
+ }
1633
+ if (options.signal) {
1634
+ waiter.onAbort = () => rejectWaiter(this.createAbortError());
1635
+ options.signal.addEventListener('abort', waiter.onAbort, { once: true });
1636
+ }
1637
+ this.processedSlotWaiters.add(waiter);
1638
+ });
1639
+ }
969
1640
  /**
970
1641
  * Clean up any pending timers. Call when disposing the processor.
971
1642
  */
@@ -992,31 +1663,115 @@ class FrameProcessor {
992
1663
  this.isProcessing = true;
993
1664
  const batch = this.pendingUpdates;
994
1665
  this.pendingUpdates = [];
995
- const viewsToEnforce = new Set();
996
- for (const { frame } of batch) {
997
- const viewPath = this.processFrameWithoutEnforce(frame);
998
- if (viewPath) {
999
- viewsToEnforce.add(viewPath);
1666
+ try {
1667
+ const viewsToEnforce = new Set();
1668
+ for (const { frame } of batch) {
1669
+ const viewPath = this.processFrameWithoutEnforce(frame);
1670
+ if (viewPath) {
1671
+ viewsToEnforce.add(viewPath);
1672
+ }
1673
+ }
1674
+ viewsToEnforce.forEach((viewPath) => {
1675
+ this.enforceMaxEntries(viewPath);
1676
+ });
1677
+ for (const { frame } of batch) {
1678
+ this.markFrameProcessed(frame);
1000
1679
  }
1001
1680
  }
1002
- viewsToEnforce.forEach((viewPath) => {
1003
- this.enforceMaxEntries(viewPath);
1004
- });
1005
- this.isProcessing = false;
1681
+ finally {
1682
+ this.isProcessing = false;
1683
+ }
1006
1684
  }
1007
1685
  processFrame(frame) {
1008
- if (isSubscribedFrame(frame)) {
1686
+ if (isErrorFrame(frame)) {
1687
+ this.queryStore?.failFrame(frame);
1688
+ }
1689
+ else if (isUnsubscribedFrame(frame)) ;
1690
+ else if (isSubscribedFrame(frame)) {
1009
1691
  this.handleSubscribedFrame(frame);
1010
1692
  }
1011
1693
  else if (isSnapshotFrame(frame)) {
1012
1694
  this.handleSnapshotFrame(frame);
1013
1695
  }
1014
- else {
1696
+ else if (isEntityFrame(frame)) {
1015
1697
  this.handleEntityFrame(frame);
1016
1698
  }
1699
+ this.markFrameProcessed(frame);
1700
+ }
1701
+ normalizeSlot(slot) {
1702
+ if (typeof slot === 'number') {
1703
+ if (!Number.isSafeInteger(slot) || slot < 0) {
1704
+ throw new RangeError('slot must be a non-negative safe integer or bigint');
1705
+ }
1706
+ return BigInt(slot);
1707
+ }
1708
+ if (slot < 0n) {
1709
+ throw new RangeError('slot must be non-negative');
1710
+ }
1711
+ return slot;
1712
+ }
1713
+ slotFromSeq(seq) {
1714
+ if (typeof seq !== 'string') {
1715
+ return null;
1716
+ }
1717
+ const slot = seq.split(':', 1)[0];
1718
+ return slot && /^\d+$/.test(slot) ? BigInt(slot) : null;
1719
+ }
1720
+ frameSlot(frame) {
1721
+ if (isSubscribedFrame(frame) || isUnsubscribedFrame(frame) || isErrorFrame(frame)) {
1722
+ return null;
1723
+ }
1724
+ if (isSnapshotFrame(frame)) {
1725
+ let latest = null;
1726
+ for (const entity of frame.data) {
1727
+ const slot = isObject(entity.data)
1728
+ ? this.slotFromSeq(entity.data._seq)
1729
+ : null;
1730
+ if (slot !== null && (latest === null || slot > latest)) {
1731
+ latest = slot;
1732
+ }
1733
+ }
1734
+ return latest;
1735
+ }
1736
+ return this.slotFromSeq(frame.seq)
1737
+ ?? (isObject(frame.data) ? this.slotFromSeq(frame.data._seq) : null);
1738
+ }
1739
+ markFrameProcessed(frame) {
1740
+ const slot = this.frameSlot(frame);
1741
+ if (slot === null || (this.latestProcessedSlot !== null && slot <= this.latestProcessedSlot)) {
1742
+ return;
1743
+ }
1744
+ this.latestProcessedSlot = slot;
1745
+ for (const waiter of [...this.processedSlotWaiters]) {
1746
+ if (slot >= waiter.targetSlot) {
1747
+ this.removeProcessedSlotWaiter(waiter);
1748
+ waiter.resolve(slot);
1749
+ }
1750
+ }
1751
+ }
1752
+ removeProcessedSlotWaiter(waiter) {
1753
+ this.processedSlotWaiters.delete(waiter);
1754
+ if (waiter.timeout !== null) {
1755
+ clearTimeout(waiter.timeout);
1756
+ }
1757
+ if (waiter.signal && waiter.onAbort) {
1758
+ waiter.signal.removeEventListener('abort', waiter.onAbort);
1759
+ }
1760
+ }
1761
+ createAbortError() {
1762
+ const error = new Error('Waiting for processed slot was aborted');
1763
+ error.name = 'AbortError';
1764
+ return error;
1017
1765
  }
1018
1766
  processFrameWithoutEnforce(frame) {
1019
- if (isSubscribedFrame(frame)) {
1767
+ if (isErrorFrame(frame)) {
1768
+ this.queryStore?.failFrame(frame);
1769
+ return null;
1770
+ }
1771
+ else if (isUnsubscribedFrame(frame)) {
1772
+ return null;
1773
+ }
1774
+ else if (isSubscribedFrame(frame)) {
1020
1775
  this.handleSubscribedFrame(frame);
1021
1776
  return null;
1022
1777
  }
@@ -1024,17 +1779,20 @@ class FrameProcessor {
1024
1779
  this.handleSnapshotFrameWithoutEnforce(frame);
1025
1780
  return frame.entity;
1026
1781
  }
1027
- else {
1782
+ else if (isEntityFrame(frame)) {
1028
1783
  this.handleEntityFrameWithoutEnforce(frame);
1029
1784
  return frame.entity;
1030
1785
  }
1786
+ return null;
1031
1787
  }
1032
1788
  handleSubscribedFrame(frame) {
1789
+ const viewPath = frame.query.view;
1033
1790
  if (this.storage.setViewConfig && frame.sort) {
1034
- this.storage.setViewConfig(frame.view, {
1035
- sort: this.normalizeSortConfig(frame.view, frame.sort),
1791
+ this.storage.setViewConfig(viewPath, {
1792
+ sort: this.normalizeSortConfig(viewPath, frame.sort),
1036
1793
  });
1037
1794
  }
1795
+ this.queryStore?.acknowledge(frame.subscriptionId, frame.query, frame.mode, frame.sort ? this.normalizeSortConfig(viewPath, frame.sort) : undefined);
1038
1796
  }
1039
1797
  handleSnapshotFrame(frame) {
1040
1798
  this.handleSnapshotFrameWithoutEnforce(frame);
@@ -1042,12 +1800,18 @@ class FrameProcessor {
1042
1800
  }
1043
1801
  handleSnapshotFrameWithoutEnforce(frame) {
1044
1802
  const viewPath = frame.entity;
1803
+ const acceptedKeys = [];
1045
1804
  for (const entity of frame.data) {
1046
- const normalized = this.normalizeEntity(viewPath, entity.data);
1805
+ const normalized = this.normalizeEntity(viewPath, entity.data, {
1806
+ key: entity.key,
1807
+ operation: 'snapshot',
1808
+ seq: this.extractSeq(entity.data),
1809
+ });
1047
1810
  if (normalized === null) {
1048
1811
  continue;
1049
1812
  }
1050
1813
  const nextValue = this.attachInternalSeq(viewPath, normalized, this.extractSeq(entity.data));
1814
+ acceptedKeys.push(entity.key);
1051
1815
  const previousValue = this.storage.get(viewPath, entity.key);
1052
1816
  this.storage.set(viewPath, entity.key, nextValue);
1053
1817
  this.storage.notifyUpdate(viewPath, entity.key, {
@@ -1057,6 +1821,7 @@ class FrameProcessor {
1057
1821
  });
1058
1822
  this.emitRichUpdate(viewPath, entity.key, previousValue, nextValue, 'upsert');
1059
1823
  }
1824
+ this.queryStore?.stageSnapshot(frame, acceptedKeys);
1060
1825
  }
1061
1826
  handleEntityFrame(frame) {
1062
1827
  this.handleEntityFrameWithoutEnforce(frame);
@@ -1065,44 +1830,86 @@ class FrameProcessor {
1065
1830
  handleEntityFrameWithoutEnforce(frame) {
1066
1831
  const viewPath = frame.entity;
1067
1832
  const previousValue = this.storage.get(viewPath, frame.key);
1833
+ const previousSequence = this.getInternalSeq(previousValue);
1834
+ const duplicateOrStaleSequence = frame.seq !== undefined
1835
+ && previousSequence !== undefined
1836
+ && this.compareSeq(frame.seq, previousSequence) <= 0;
1068
1837
  switch (frame.op) {
1069
- case 'create':
1070
1838
  case 'upsert':
1071
1839
  {
1072
- const normalized = this.normalizeEntity(viewPath, frame.data);
1840
+ if (frame.data === null)
1841
+ break;
1842
+ if (duplicateOrStaleSequence && previousValue !== null) {
1843
+ const update = {
1844
+ type: 'upsert',
1845
+ key: frame.key,
1846
+ data: previousValue,
1847
+ };
1848
+ this.queryStore?.applyLive(frame.subscriptionId, frame.key, update, this.createRichUpdate(frame.key, null, previousValue), frame.seq);
1849
+ break;
1850
+ }
1851
+ const normalized = this.normalizeEntity(viewPath, frame.data, {
1852
+ key: frame.key,
1853
+ operation: 'upsert',
1854
+ seq: frame.seq ?? this.extractSeq(frame.data),
1855
+ });
1073
1856
  if (normalized === null) {
1074
1857
  break;
1075
1858
  }
1076
1859
  const nextValue = this.attachInternalSeq(viewPath, normalized, frame.seq ?? this.extractSeq(frame.data));
1077
1860
  this.storage.set(viewPath, frame.key, nextValue);
1078
- this.storage.notifyUpdate(viewPath, frame.key, {
1861
+ const update = {
1079
1862
  type: 'upsert',
1080
1863
  key: frame.key,
1081
1864
  data: nextValue,
1082
- });
1083
- this.emitRichUpdate(viewPath, frame.key, previousValue, nextValue, frame.op);
1865
+ };
1866
+ this.storage.notifyUpdate(viewPath, frame.key, update);
1867
+ const richUpdate = this.createRichUpdate(frame.key, previousValue, nextValue);
1868
+ this.storage.notifyRichUpdate(viewPath, frame.key, richUpdate);
1869
+ this.queryStore?.applyLive(frame.subscriptionId, frame.key, update, richUpdate, frame.seq ?? this.extractSeq(frame.data));
1084
1870
  break;
1085
1871
  }
1086
1872
  case 'patch': {
1873
+ if (frame.data === null)
1874
+ break;
1087
1875
  const existing = this.storage.get(viewPath, frame.key);
1088
- const normalizedPatch = this.normalizeEntity(viewPath, frame.data, true);
1876
+ const normalizedPatch = this.normalizeEntity(viewPath, frame.data, {
1877
+ key: frame.key,
1878
+ operation: 'patch',
1879
+ seq: frame.seq ?? this.extractSeq(frame.data),
1880
+ }, true);
1089
1881
  if (normalizedPatch === null) {
1090
1882
  break;
1091
1883
  }
1884
+ if (duplicateOrStaleSequence && existing !== null) {
1885
+ const update = {
1886
+ type: 'patch',
1887
+ key: frame.key,
1888
+ data: normalizedPatch,
1889
+ };
1890
+ this.queryStore?.applyLive(frame.subscriptionId, frame.key, update, this.createRichUpdate(frame.key, existing, existing, normalizedPatch), frame.seq);
1891
+ break;
1892
+ }
1092
1893
  const appendPaths = this.normalizeAppendPaths(viewPath, frame.append);
1093
1894
  const merged = existing
1094
1895
  ? deepMergeWithAppend(existing, normalizedPatch, appendPaths)
1095
1896
  : normalizedPatch;
1096
1897
  const nextValue = this.attachInternalSeq(viewPath, merged, frame.seq ?? this.extractSeq(frame.data) ?? this.getInternalSeq(existing));
1097
1898
  this.storage.set(viewPath, frame.key, nextValue);
1098
- this.storage.notifyUpdate(viewPath, frame.key, {
1899
+ const update = {
1099
1900
  type: 'patch',
1100
1901
  key: frame.key,
1101
1902
  data: normalizedPatch,
1102
- });
1103
- this.emitRichUpdate(viewPath, frame.key, previousValue, nextValue, 'patch', normalizedPatch);
1903
+ };
1904
+ this.storage.notifyUpdate(viewPath, frame.key, update);
1905
+ const richUpdate = this.createRichUpdate(frame.key, previousValue, nextValue, normalizedPatch);
1906
+ this.storage.notifyRichUpdate(viewPath, frame.key, richUpdate);
1907
+ this.queryStore?.applyLive(frame.subscriptionId, frame.key, update, richUpdate, frame.seq ?? this.extractSeq(frame.data));
1104
1908
  break;
1105
1909
  }
1910
+ case 'remove':
1911
+ this.queryStore?.applyLive(frame.subscriptionId, frame.key, { type: 'remove', key: frame.key });
1912
+ break;
1106
1913
  case 'delete':
1107
1914
  this.storage.delete(viewPath, frame.key);
1108
1915
  this.storage.notifyUpdate(viewPath, frame.key, {
@@ -1113,6 +1920,7 @@ class FrameProcessor {
1113
1920
  const richUpdate = { type: 'deleted', key: frame.key, lastKnown: previousValue };
1114
1921
  this.storage.notifyRichUpdate(viewPath, frame.key, richUpdate);
1115
1922
  }
1923
+ this.queryStore?.deleteGlobal(viewPath, frame.key, previousValue ?? undefined);
1116
1924
  break;
1117
1925
  }
1118
1926
  }
@@ -1122,13 +1930,20 @@ class FrameProcessor {
1122
1930
  : { type: 'updated', key, before, after, patch };
1123
1931
  this.storage.notifyRichUpdate(viewPath, key, richUpdate);
1124
1932
  }
1933
+ createRichUpdate(key, before, after, patch) {
1934
+ return before === null
1935
+ ? { type: 'created', key, data: after }
1936
+ : { type: 'updated', key, before, after, patch };
1937
+ }
1125
1938
  enforceMaxEntries(viewPath) {
1126
1939
  if (this.maxEntriesPerView === null)
1127
1940
  return;
1128
1941
  if (!this.storage.evictOldest)
1129
1942
  return;
1130
1943
  while (this.storage.size(viewPath) > this.maxEntriesPerView) {
1131
- this.storage.evictOldest(viewPath);
1944
+ const evicted = this.storage.evictOldest(viewPath);
1945
+ if (evicted !== undefined)
1946
+ this.queryStore?.evict(viewPath, evicted);
1132
1947
  }
1133
1948
  }
1134
1949
  }
@@ -1275,7 +2090,7 @@ class MemoryAdapter {
1275
2090
  }
1276
2091
  }
1277
2092
 
1278
- function getNestedValue(obj, path) {
2093
+ function getNestedValue$1(obj, path) {
1279
2094
  let current = obj;
1280
2095
  for (const segment of path) {
1281
2096
  if (current === null || current === undefined)
@@ -1431,7 +2246,7 @@ class SortedStorageDecorator {
1431
2246
  sortedKeys.splice(insertIdx, 0, key);
1432
2247
  }
1433
2248
  binarySearchInsertPosition(viewPath, sortedKeys, sortConfig, newKey, newValue) {
1434
- const newSortValue = getNestedValue(newValue, sortConfig.field);
2249
+ const newSortValue = getNestedValue$1(newValue, sortConfig.field);
1435
2250
  const isDesc = sortConfig.order === 'desc';
1436
2251
  let low = 0;
1437
2252
  let high = sortedKeys.length;
@@ -1439,7 +2254,7 @@ class SortedStorageDecorator {
1439
2254
  const mid = Math.floor((low + high) / 2);
1440
2255
  const midKey = sortedKeys[mid];
1441
2256
  const midEntity = this.inner.get(viewPath, midKey);
1442
- const midValue = getNestedValue(midEntity, sortConfig.field);
2257
+ const midValue = getNestedValue$1(midEntity, sortConfig.field);
1443
2258
  let cmp = compareSortValues(newSortValue, midValue);
1444
2259
  if (isDesc)
1445
2260
  cmp = -cmp;
@@ -1462,8 +2277,8 @@ class SortedStorageDecorator {
1462
2277
  const isDesc = sortConfig.order === 'desc';
1463
2278
  const entries = allKeys.map(k => [k, this.inner.get(viewPath, k)]);
1464
2279
  entries.sort((a, b) => {
1465
- const aValue = getNestedValue(a[1], sortConfig.field);
1466
- const bValue = getNestedValue(b[1], sortConfig.field);
2280
+ const aValue = getNestedValue$1(a[1], sortConfig.field);
2281
+ const bValue = getNestedValue$1(b[1], sortConfig.field);
1467
2282
  let cmp = compareSortValues(aValue, bValue);
1468
2283
  if (isDesc)
1469
2284
  cmp = -cmp;
@@ -1476,303 +2291,584 @@ class SortedStorageDecorator {
1476
2291
  }
1477
2292
  }
1478
2293
 
1479
- class SubscriptionRegistry {
1480
- constructor(connection) {
1481
- this.subscriptions = new Map();
1482
- this.connection = connection;
2294
+ function getNestedValue(value, path) {
2295
+ let current = value;
2296
+ for (const segment of path) {
2297
+ if (current === null || typeof current !== 'object')
2298
+ return undefined;
2299
+ current = current[segment];
1483
2300
  }
1484
- subscribe(subscription) {
1485
- const subKey = this.makeSubKey(subscription);
1486
- const existing = this.subscriptions.get(subKey);
1487
- if (existing) {
1488
- existing.refCount++;
2301
+ return current;
2302
+ }
2303
+ function compareValues(left, right) {
2304
+ if (left === right)
2305
+ return 0;
2306
+ if (left === undefined || left === null)
2307
+ return -1;
2308
+ if (right === undefined || right === null)
2309
+ return 1;
2310
+ if (typeof left === 'number' && typeof right === 'number')
2311
+ return left - right;
2312
+ if (typeof left === 'bigint' && typeof right === 'bigint')
2313
+ return left < right ? -1 : 1;
2314
+ if (typeof left === 'boolean' && typeof right === 'boolean') {
2315
+ return Number(left) - Number(right);
2316
+ }
2317
+ return String(left).localeCompare(String(right));
2318
+ }
2319
+ function compareSequences(left, right) {
2320
+ if (typeof left !== 'string' || typeof right !== 'string')
2321
+ return compareValues(left, right);
2322
+ const [leftSlot, leftIndex = ''] = left.split(':', 2);
2323
+ const [rightSlot, rightIndex = ''] = right.split(':', 2);
2324
+ if (/^\d+$/.test(leftSlot ?? '') && /^\d+$/.test(rightSlot ?? '')) {
2325
+ const leftValue = BigInt(leftSlot);
2326
+ const rightValue = BigInt(rightSlot);
2327
+ if (leftValue !== rightValue)
2328
+ return leftValue < rightValue ? -1 : 1;
2329
+ }
2330
+ return leftIndex.localeCompare(rightIndex);
2331
+ }
2332
+ function appendUnique(target, keys) {
2333
+ for (const key of keys) {
2334
+ if (!target.includes(key))
2335
+ target.push(key);
2336
+ }
2337
+ }
2338
+ class QueryStore {
2339
+ constructor(storage) {
2340
+ this.storage = storage;
2341
+ this.records = new Map();
2342
+ }
2343
+ register(subscription, queryKey) {
2344
+ if (this.records.has(subscription.subscriptionId))
2345
+ return;
2346
+ this.records.set(subscription.subscriptionId, {
2347
+ subscription,
2348
+ queryKey,
2349
+ keys: [],
2350
+ sequences: new Map(),
2351
+ isLoading: subscription.snapshot.enabled,
2352
+ isRefreshing: false,
2353
+ resolved: !subscription.snapshot.enabled,
2354
+ listeners: new Set(),
2355
+ updateListeners: new Set(),
2356
+ richUpdateListeners: new Set(),
2357
+ version: 0,
2358
+ cachedVersion: -1,
2359
+ });
2360
+ }
2361
+ unregister(subscriptionId) {
2362
+ const record = this.records.get(subscriptionId);
2363
+ if (!record)
2364
+ return;
2365
+ this.rejectRefresh(record, new AreteError('Subscription was released while refreshing', 'SUBSCRIPTION_NOT_FOUND'));
2366
+ this.records.delete(subscriptionId);
2367
+ }
2368
+ getSubscription(subscriptionId) {
2369
+ return this.records.get(subscriptionId)?.subscription;
2370
+ }
2371
+ getQueryKey(subscriptionId) {
2372
+ return this.records.get(subscriptionId)?.queryKey;
2373
+ }
2374
+ getSnapshot(subscriptionId) {
2375
+ const record = this.records.get(subscriptionId);
2376
+ if (!record)
2377
+ return undefined;
2378
+ if (record.cachedSnapshot && record.cachedVersion === record.version) {
2379
+ return record.cachedSnapshot;
1489
2380
  }
1490
- else {
1491
- this.subscriptions.set(subKey, {
1492
- subscription,
1493
- refCount: 1,
1494
- });
1495
- this.connection.subscribe(subscription);
2381
+ const data = record.keys.flatMap((key) => {
2382
+ const entity = this.storage.get(record.subscription.query.view, key);
2383
+ return entity === null ? [] : [entity];
2384
+ });
2385
+ const snapshot = {
2386
+ subscriptionId,
2387
+ query: record.subscription.query,
2388
+ keys: record.keys,
2389
+ data,
2390
+ isLoading: record.isLoading,
2391
+ isRefreshing: record.isRefreshing,
2392
+ ...(record.error ? { error: record.error } : {}),
2393
+ };
2394
+ record.cachedSnapshot = snapshot;
2395
+ record.cachedVersion = record.version;
2396
+ return snapshot;
2397
+ }
2398
+ onChange(subscriptionId, callback) {
2399
+ const listeners = this.requireRecord(subscriptionId).listeners;
2400
+ listeners.add(callback);
2401
+ return () => listeners.delete(callback);
2402
+ }
2403
+ onUpdate(subscriptionId, callback) {
2404
+ const listeners = this.requireRecord(subscriptionId).updateListeners;
2405
+ listeners.add(callback);
2406
+ return () => listeners.delete(callback);
2407
+ }
2408
+ onRichUpdate(subscriptionId, callback) {
2409
+ const listeners = this.requireRecord(subscriptionId).richUpdateListeners;
2410
+ listeners.add(callback);
2411
+ return () => listeners.delete(callback);
2412
+ }
2413
+ acknowledge(subscriptionId, effectiveQuery, mode, sort) {
2414
+ const record = this.records.get(subscriptionId);
2415
+ if (!record)
2416
+ return;
2417
+ record.mode = mode;
2418
+ record.sort = sort;
2419
+ record.error = undefined;
2420
+ if (!record.subscription.snapshot.enabled) {
2421
+ record.isLoading = false;
2422
+ record.isRefreshing = false;
2423
+ record.resolved = true;
2424
+ }
2425
+ if (effectiveQuery.view !== record.subscription.query.view) {
2426
+ this.fail(subscriptionId, new AreteError('Server acknowledged a different view for the subscription', 'INVALID_FRAME'));
2427
+ return;
1496
2428
  }
1497
- return () => this.unsubscribe(subscription);
2429
+ this.touch(record);
1498
2430
  }
1499
- unsubscribe(subscription) {
1500
- const subKey = this.makeSubKey(subscription);
1501
- const existing = this.subscriptions.get(subKey);
1502
- if (existing) {
1503
- existing.refCount--;
1504
- if (existing.refCount <= 0) {
1505
- this.subscriptions.delete(subKey);
1506
- this.connection.unsubscribe(subscription.view, subscription.key);
2431
+ stageSnapshot(frame, keys) {
2432
+ const record = this.records.get(frame.subscriptionId);
2433
+ if (!record)
2434
+ return;
2435
+ if (!record.staged
2436
+ || record.staged.snapshotId !== frame.snapshotId) {
2437
+ record.staged = {
2438
+ snapshotId: frame.snapshotId,
2439
+ authoritative: frame.authoritative,
2440
+ keys: [],
2441
+ };
2442
+ }
2443
+ const staged = record.staged;
2444
+ if (staged.authoritative !== frame.authoritative) {
2445
+ this.fail(frame.subscriptionId, new AreteError('Snapshot batches disagree on authoritative mode', 'INVALID_FRAME'));
2446
+ record.staged = undefined;
2447
+ return;
2448
+ }
2449
+ appendUnique(staged.keys, keys);
2450
+ if (!frame.complete)
2451
+ return;
2452
+ const stagedKeys = staged.keys;
2453
+ if (frame.authoritative) {
2454
+ const retained = new Set(stagedKeys);
2455
+ for (const key of record.sequences.keys()) {
2456
+ if (!retained.has(key))
2457
+ record.sequences.delete(key);
2458
+ }
2459
+ }
2460
+ for (const key of stagedKeys) {
2461
+ const sequence = this.entitySequence(record.subscription.query.view, key);
2462
+ if (sequence !== undefined)
2463
+ record.sequences.set(key, sequence);
2464
+ }
2465
+ record.keys = frame.authoritative
2466
+ ? [...stagedKeys]
2467
+ : this.mergeIncremental(record.keys, stagedKeys);
2468
+ record.staged = undefined;
2469
+ record.isLoading = false;
2470
+ record.isRefreshing = false;
2471
+ record.resolved = true;
2472
+ record.error = undefined;
2473
+ this.touch(record);
2474
+ this.resolveRefresh(record);
2475
+ for (const key of stagedKeys) {
2476
+ const data = this.storage.get(record.subscription.query.view, key);
2477
+ if (data !== null) {
2478
+ this.emitUpdate(record, { type: 'upsert', key, data });
2479
+ this.emitRichUpdate(record, { type: 'created', key, data });
1507
2480
  }
1508
2481
  }
1509
2482
  }
1510
- getRefCount(subscription) {
1511
- const subKey = this.makeSubKey(subscription);
1512
- return this.subscriptions.get(subKey)?.refCount ?? 0;
2483
+ applyLive(subscriptionId, key, update, richUpdate, sequence) {
2484
+ const record = this.records.get(subscriptionId);
2485
+ if (!record)
2486
+ return;
2487
+ if (update.type === 'remove') {
2488
+ const lastKnown = this.storage.get(record.subscription.query.view, key) ?? undefined;
2489
+ record.keys = record.keys.filter((entry) => entry !== key);
2490
+ record.sequences.delete(key);
2491
+ this.touch(record);
2492
+ this.emitUpdate(record, update);
2493
+ this.emitRichUpdate(record, { type: 'removed', key, lastKnown });
2494
+ return;
2495
+ }
2496
+ if (!record.keys.includes(key))
2497
+ record.keys = [...record.keys, key];
2498
+ const nextSequence = sequence ?? this.entitySequence(record.subscription.query.view, key);
2499
+ if (nextSequence !== undefined)
2500
+ record.sequences.set(key, nextSequence);
2501
+ this.sortKeys(record);
2502
+ record.isLoading = false;
2503
+ if (!record.refreshWaiter)
2504
+ record.isRefreshing = false;
2505
+ record.resolved = true;
2506
+ record.error = undefined;
2507
+ this.touch(record);
2508
+ this.emitUpdate(record, update);
2509
+ if (richUpdate)
2510
+ this.emitRichUpdate(record, richUpdate);
2511
+ }
2512
+ deleteGlobal(view, key, lastKnown) {
2513
+ for (const record of this.records.values()) {
2514
+ if (record.subscription.query.view !== view || !record.keys.includes(key))
2515
+ continue;
2516
+ record.keys = record.keys.filter((entry) => entry !== key);
2517
+ record.sequences.delete(key);
2518
+ this.touch(record);
2519
+ this.emitUpdate(record, { type: 'delete', key });
2520
+ this.emitRichUpdate(record, { type: 'deleted', key, lastKnown });
2521
+ }
1513
2522
  }
1514
- getActiveSubscriptions() {
1515
- return Array.from(this.subscriptions.values()).map((t) => t.subscription);
2523
+ evict(view, key) {
2524
+ for (const record of this.records.values()) {
2525
+ if (record.subscription.query.view !== view || !record.keys.includes(key))
2526
+ continue;
2527
+ record.keys = record.keys.filter((entry) => entry !== key);
2528
+ record.sequences.delete(key);
2529
+ this.touch(record);
2530
+ }
2531
+ }
2532
+ failFrame(frame) {
2533
+ if (frame.subscriptionId === null)
2534
+ return;
2535
+ this.fail(frame.subscriptionId, new AreteError(frame.message ?? frame.error ?? frame.code, frame.code, frame));
2536
+ }
2537
+ beginRefresh(subscriptionId, waitForSnapshot = false) {
2538
+ const record = this.requireRecord(subscriptionId);
2539
+ record.error = undefined;
2540
+ record.isLoading = !record.resolved && record.subscription.snapshot.enabled;
2541
+ record.isRefreshing = record.resolved && record.subscription.snapshot.enabled;
2542
+ record.staged = undefined;
2543
+ this.touch(record);
2544
+ if (!waitForSnapshot || !record.subscription.snapshot.enabled) {
2545
+ return Promise.resolve();
2546
+ }
2547
+ if (record.refreshWaiter)
2548
+ return record.refreshWaiter.promise;
2549
+ let resolve;
2550
+ let reject;
2551
+ const promise = new Promise((resolvePromise, rejectPromise) => {
2552
+ resolve = resolvePromise;
2553
+ reject = rejectPromise;
2554
+ });
2555
+ record.refreshWaiter = { promise, resolve, reject };
2556
+ return promise;
2557
+ }
2558
+ failRefresh(subscriptionId, error) {
2559
+ this.fail(subscriptionId, error);
2560
+ }
2561
+ beginReconnect() {
2562
+ for (const record of this.records.values()) {
2563
+ this.beginRefresh(record.subscription.subscriptionId);
2564
+ }
2565
+ }
2566
+ failRefreshing(error) {
2567
+ for (const [subscriptionId, record] of this.records) {
2568
+ if (record.isRefreshing || record.refreshWaiter) {
2569
+ this.fail(subscriptionId, error);
2570
+ }
2571
+ }
1516
2572
  }
1517
2573
  clear() {
1518
- for (const { subscription } of this.subscriptions.values()) {
1519
- this.connection.unsubscribe(subscription.view, subscription.key);
2574
+ for (const record of this.records.values()) {
2575
+ this.rejectRefresh(record, new AreteError('Subscription store was cleared while refreshing', 'CONNECTION_CANCELLED'));
1520
2576
  }
1521
- this.subscriptions.clear();
2577
+ this.records.clear();
1522
2578
  }
1523
- makeSubKey(subscription) {
1524
- const filters = subscription.filters ? JSON.stringify(subscription.filters) : '{}';
1525
- return `${subscription.view}:${subscription.key ?? '*'}:${subscription.partition ?? ''}:${filters}`;
2579
+ mergeIncremental(existing, incoming) {
2580
+ const result = [...existing];
2581
+ appendUnique(result, incoming);
2582
+ return result;
2583
+ }
2584
+ sortKeys(record) {
2585
+ if (record.keys.length < 2)
2586
+ return;
2587
+ if (!record.sort && record.mode !== 'list' && record.mode !== 'append')
2588
+ return;
2589
+ const field = record.sort?.field;
2590
+ const order = record.sort?.order ?? (record.subscription.query.after ? 'asc' : 'desc');
2591
+ const view = record.subscription.query.view;
2592
+ record.keys = [...record.keys].sort((leftKey, rightKey) => {
2593
+ const left = field
2594
+ ? getNestedValue(this.storage.get(view, leftKey), field)
2595
+ : record.sequences.get(leftKey);
2596
+ const right = field
2597
+ ? getNestedValue(this.storage.get(view, rightKey), field)
2598
+ : record.sequences.get(rightKey);
2599
+ let compared = field ? compareValues(left, right) : compareSequences(left, right);
2600
+ if (order === 'desc')
2601
+ compared = -compared;
2602
+ return compared === 0 ? leftKey.localeCompare(rightKey) : compared;
2603
+ });
2604
+ }
2605
+ entitySequence(view, key) {
2606
+ const entity = this.storage.get(view, key);
2607
+ if (entity === null || typeof entity !== 'object')
2608
+ return undefined;
2609
+ const value = entity;
2610
+ const sequence = value['__seq'] ?? value['_seq'];
2611
+ return typeof sequence === 'string' ? sequence : undefined;
2612
+ }
2613
+ fail(subscriptionId, error) {
2614
+ const record = this.records.get(subscriptionId);
2615
+ if (!record)
2616
+ return;
2617
+ record.error = error;
2618
+ record.isLoading = false;
2619
+ record.isRefreshing = false;
2620
+ record.staged = undefined;
2621
+ this.touch(record);
2622
+ this.rejectRefresh(record, error);
2623
+ }
2624
+ resolveRefresh(record) {
2625
+ const waiter = record.refreshWaiter;
2626
+ if (!waiter)
2627
+ return;
2628
+ record.refreshWaiter = undefined;
2629
+ waiter.resolve();
2630
+ }
2631
+ rejectRefresh(record, error) {
2632
+ const waiter = record.refreshWaiter;
2633
+ if (!waiter)
2634
+ return;
2635
+ record.refreshWaiter = undefined;
2636
+ waiter.reject(error);
2637
+ }
2638
+ requireRecord(subscriptionId) {
2639
+ const record = this.records.get(subscriptionId);
2640
+ if (!record) {
2641
+ throw new AreteError(`Unknown local subscription '${subscriptionId}'`, 'SUBSCRIPTION_NOT_FOUND');
2642
+ }
2643
+ return record;
2644
+ }
2645
+ touch(record) {
2646
+ record.version++;
2647
+ for (const listener of record.listeners)
2648
+ listener();
2649
+ }
2650
+ emitUpdate(record, update) {
2651
+ for (const listener of record.updateListeners)
2652
+ listener(update);
2653
+ }
2654
+ emitRichUpdate(record, update) {
2655
+ for (const listener of record.richUpdateListeners)
2656
+ listener(update);
1526
2657
  }
1527
2658
  }
1528
2659
 
1529
2660
  const MAX_QUEUE_SIZE = 1000;
1530
- function createUpdateStream(storage, subscriptionRegistry, subscription, keyFilter) {
2661
+ function enqueue(queue, value) {
2662
+ if (queue.length >= MAX_QUEUE_SIZE)
2663
+ queue.shift();
2664
+ queue.push(value);
2665
+ }
2666
+ function createIterator(start) {
2667
+ const queue = [];
2668
+ let waitingResolve = null;
2669
+ let done = false;
2670
+ const push = (value) => {
2671
+ if (waitingResolve) {
2672
+ const resolve = waitingResolve;
2673
+ waitingResolve = null;
2674
+ resolve({ value, done: false });
2675
+ }
2676
+ else {
2677
+ enqueue(queue, value);
2678
+ }
2679
+ };
2680
+ const stop = start(push);
2681
+ const cleanup = () => {
2682
+ if (done)
2683
+ return;
2684
+ done = true;
2685
+ stop();
2686
+ waitingResolve?.({ value: undefined, done: true });
2687
+ waitingResolve = null;
2688
+ };
2689
+ return {
2690
+ async next() {
2691
+ if (done)
2692
+ return { value: undefined, done: true };
2693
+ const value = queue.shift();
2694
+ if (value !== undefined)
2695
+ return { value, done: false };
2696
+ return new Promise((resolve) => {
2697
+ waitingResolve = resolve;
2698
+ });
2699
+ },
2700
+ async return() {
2701
+ cleanup();
2702
+ return { value: undefined, done: true };
2703
+ },
2704
+ async throw(error) {
2705
+ cleanup();
2706
+ throw error;
2707
+ },
2708
+ };
2709
+ }
2710
+ function acquire(registry, query, snapshotEnabled) {
2711
+ return registry.subscribe(query, snapshotEnabled);
2712
+ }
2713
+ function createUpdateStream(_storage, subscriptionRegistry, query, keyFilter, snapshotEnabled = true) {
1531
2714
  return {
1532
2715
  [Symbol.asyncIterator]() {
1533
- const queue = [];
1534
- let waitingResolve = null;
1535
- let unsubscribeStorage = null;
1536
- let unsubscribeRegistry = null;
1537
- let done = false;
1538
- const handler = (viewPath, key, update) => {
1539
- if (viewPath !== subscription.view)
1540
- return;
1541
- if (keyFilter !== undefined && key !== keyFilter)
1542
- return;
1543
- const typedUpdate = update;
1544
- if (waitingResolve) {
1545
- const resolve = waitingResolve;
1546
- waitingResolve = null;
1547
- resolve({ value: typedUpdate, done: false });
1548
- }
1549
- else {
1550
- if (queue.length >= MAX_QUEUE_SIZE) {
1551
- queue.shift();
1552
- }
1553
- queue.push({
1554
- update: typedUpdate,
1555
- resolve: () => { },
1556
- });
1557
- }
1558
- };
1559
- const start = () => {
1560
- unsubscribeStorage = storage.onUpdate(handler);
1561
- unsubscribeRegistry = subscriptionRegistry.subscribe(subscription);
1562
- };
1563
- const cleanup = () => {
1564
- done = true;
1565
- unsubscribeStorage?.();
1566
- unsubscribeRegistry?.();
1567
- };
1568
- start();
1569
- return {
1570
- async next() {
1571
- if (done) {
1572
- return { value: undefined, done: true };
1573
- }
1574
- const queued = queue.shift();
1575
- if (queued) {
1576
- return { value: queued.update, done: false };
1577
- }
1578
- return new Promise((resolve) => {
1579
- waitingResolve = resolve;
1580
- });
1581
- },
1582
- async return() {
1583
- cleanup();
1584
- return { value: undefined, done: true };
1585
- },
1586
- async throw(error) {
1587
- cleanup();
1588
- throw error;
1589
- },
1590
- };
2716
+ return createIterator((push) => {
2717
+ const lease = acquire(subscriptionRegistry, query, snapshotEnabled);
2718
+ const unsubscribe = lease.onUpdate((update) => {
2719
+ if (keyFilter === undefined || update.key === keyFilter)
2720
+ push(update);
2721
+ });
2722
+ return () => {
2723
+ unsubscribe();
2724
+ lease.release();
2725
+ };
2726
+ });
1591
2727
  },
1592
2728
  };
1593
2729
  }
1594
- function createEntityStream(storage, subscriptionRegistry, subscription, options, keyFilter) {
2730
+ function createEntityStream(_storage, subscriptionRegistry, query, options, keyFilter) {
1595
2731
  const schema = options?.schema;
1596
2732
  return {
1597
2733
  [Symbol.asyncIterator]() {
1598
- const queue = [];
1599
- let waitingResolve = null;
1600
- let unsubscribeStorage = null;
1601
- let unsubscribeRegistry = null;
1602
- let done = false;
1603
- const handler = (viewPath, key, update) => {
1604
- if (viewPath !== subscription.view)
1605
- return;
1606
- if (keyFilter !== undefined && key !== keyFilter)
1607
- return;
1608
- if (update.type === 'deleted')
1609
- return;
1610
- const entity = (update.type === 'created' ? update.data : update.after);
1611
- let output;
1612
- if (schema) {
1613
- const parsed = schema.safeParse(entity);
1614
- if (!parsed.success) {
2734
+ return createIterator((push) => {
2735
+ const lease = acquire(subscriptionRegistry, query, options?.withSnapshot ?? true);
2736
+ const emit = (entity) => {
2737
+ if (!schema) {
2738
+ push(entity);
1615
2739
  return;
1616
2740
  }
1617
- output = parsed.data;
1618
- }
1619
- else {
1620
- output = entity;
1621
- }
1622
- if (waitingResolve) {
1623
- const resolve = waitingResolve;
1624
- waitingResolve = null;
1625
- resolve({ value: output, done: false });
1626
- }
1627
- else {
1628
- if (queue.length >= MAX_QUEUE_SIZE) {
1629
- queue.shift();
1630
- }
1631
- queue.push(output);
2741
+ const parsed = schema.safeParse(entity);
2742
+ if (parsed.success)
2743
+ push(parsed.data);
2744
+ };
2745
+ for (const [index, entity] of lease.getSnapshot().data.entries()) {
2746
+ const key = lease.getSnapshot().keys[index];
2747
+ if (keyFilter === undefined || key === keyFilter)
2748
+ emit(entity);
1632
2749
  }
1633
- };
1634
- const start = () => {
1635
- unsubscribeStorage = storage.onRichUpdate(handler);
1636
- unsubscribeRegistry = subscriptionRegistry.subscribe(subscription);
1637
- };
1638
- const cleanup = () => {
1639
- done = true;
1640
- unsubscribeStorage?.();
1641
- unsubscribeRegistry?.();
1642
- };
1643
- start();
1644
- const iterator = {
1645
- async next() {
1646
- if (done) {
1647
- return { value: undefined, done: true };
1648
- }
1649
- const queued = queue.shift();
1650
- if (queued) {
1651
- return { value: queued, done: false };
1652
- }
1653
- return new Promise((resolve) => {
1654
- waitingResolve = resolve;
1655
- });
1656
- },
1657
- async return() {
1658
- cleanup();
1659
- return { value: undefined, done: true };
1660
- },
1661
- async throw(error) {
1662
- cleanup();
1663
- throw error;
1664
- },
1665
- };
1666
- return iterator;
2750
+ const unsubscribe = lease.onRichUpdate((update) => {
2751
+ if (keyFilter !== undefined && update.key !== keyFilter)
2752
+ return;
2753
+ if (update.type === 'created')
2754
+ emit(update.data);
2755
+ if (update.type === 'updated')
2756
+ emit(update.after);
2757
+ });
2758
+ return () => {
2759
+ unsubscribe();
2760
+ lease.release();
2761
+ };
2762
+ });
1667
2763
  },
1668
2764
  };
1669
2765
  }
1670
- function createRichUpdateStream(storage, subscriptionRegistry, subscription, keyFilter) {
2766
+ function createRichUpdateStream(_storage, subscriptionRegistry, query, keyFilter, snapshotEnabled = true) {
1671
2767
  return {
1672
2768
  [Symbol.asyncIterator]() {
1673
- const queue = [];
1674
- let waitingResolve = null;
1675
- let unsubscribeStorage = null;
1676
- let unsubscribeRegistry = null;
1677
- let done = false;
1678
- const handler = (viewPath, key, update) => {
1679
- if (viewPath !== subscription.view)
1680
- return;
1681
- if (keyFilter !== undefined && key !== keyFilter)
1682
- return;
1683
- const typedUpdate = update;
1684
- if (waitingResolve) {
1685
- const resolve = waitingResolve;
1686
- waitingResolve = null;
1687
- resolve({ value: typedUpdate, done: false });
1688
- }
1689
- else {
1690
- if (queue.length >= MAX_QUEUE_SIZE) {
1691
- queue.shift();
1692
- }
1693
- queue.push({
1694
- update: typedUpdate,
1695
- resolve: () => { },
1696
- });
1697
- }
1698
- };
1699
- const start = () => {
1700
- unsubscribeStorage = storage.onRichUpdate(handler);
1701
- unsubscribeRegistry = subscriptionRegistry.subscribe(subscription);
1702
- };
1703
- const cleanup = () => {
1704
- done = true;
1705
- unsubscribeStorage?.();
1706
- unsubscribeRegistry?.();
1707
- };
1708
- start();
1709
- return {
1710
- async next() {
1711
- if (done) {
1712
- return { value: undefined, done: true };
1713
- }
1714
- const queued = queue.shift();
1715
- if (queued) {
1716
- return { value: queued.update, done: false };
1717
- }
1718
- return new Promise((resolve) => {
1719
- waitingResolve = resolve;
1720
- });
1721
- },
1722
- async return() {
1723
- cleanup();
1724
- return { value: undefined, done: true };
1725
- },
1726
- async throw(error) {
1727
- cleanup();
1728
- throw error;
1729
- },
1730
- };
2769
+ return createIterator((push) => {
2770
+ const lease = acquire(subscriptionRegistry, query, snapshotEnabled);
2771
+ const unsubscribe = lease.onRichUpdate((update) => {
2772
+ if (keyFilter === undefined || update.key === keyFilter)
2773
+ push(update);
2774
+ });
2775
+ return () => {
2776
+ unsubscribe();
2777
+ lease.release();
2778
+ };
2779
+ });
1731
2780
  },
1732
2781
  };
1733
2782
  }
1734
2783
 
2784
+ function queryOptions(options) {
2785
+ const { schema: _schema, withSnapshot, ...query } = options ?? {};
2786
+ return { query, snapshotEnabled: withSnapshot ?? true };
2787
+ }
2788
+ function serializeViewKeyValue(value, view, field) {
2789
+ const location = field === undefined ? `view '${view}'` : `key field '${field}' for view '${view}'`;
2790
+ if (typeof value === 'string')
2791
+ return value;
2792
+ if (typeof value === 'bigint')
2793
+ return value.toString(10);
2794
+ if (typeof value === 'number') {
2795
+ if (!Number.isSafeInteger(value)) {
2796
+ throw new TypeError(`${location} must be a safe integer`);
2797
+ }
2798
+ return value.toString(10);
2799
+ }
2800
+ throw new TypeError(`${location} must be a string, safe integer, or bigint`);
2801
+ }
2802
+ function serializeViewKey(viewDef, key) {
2803
+ const keyFields = viewDef.keyFields ?? [];
2804
+ if (keyFields.length === 0) {
2805
+ return serializeViewKeyValue(key, viewDef.view);
2806
+ }
2807
+ if (keyFields.length !== 1) {
2808
+ throw new TypeError(`View '${viewDef.view}' has an unsupported composite key with fields [${keyFields.join(', ')}]`);
2809
+ }
2810
+ if (key === null || typeof key !== 'object' || Array.isArray(key)) {
2811
+ throw new TypeError(`View '${viewDef.view}' requires an object key`);
2812
+ }
2813
+ const field = keyFields[0];
2814
+ if (!Object.prototype.hasOwnProperty.call(key, field)) {
2815
+ throw new TypeError(`View '${viewDef.view}' key is missing field '${field}'`);
2816
+ }
2817
+ return serializeViewKeyValue(key[field], viewDef.view, field);
2818
+ }
1735
2819
  function createTypedStateView(viewDef, storage, subscriptionRegistry) {
2820
+ const wireKey = (key) => serializeViewKey(viewDef, key);
1736
2821
  return {
1737
2822
  use(key, options) {
1738
- const { schema: _schema, ...subscriptionOptions } = options ?? {};
1739
- return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...subscriptionOptions }, options, key);
2823
+ const serializedKey = wireKey(key);
2824
+ const { query } = queryOptions(options);
2825
+ return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, key: serializedKey, ...query }, options, serializedKey);
1740
2826
  },
1741
2827
  watch(key, options) {
1742
- const { schema: _schema, ...subscriptionOptions } = options ?? {};
1743
- return createUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...subscriptionOptions }, key);
2828
+ const serializedKey = wireKey(key);
2829
+ const { query, snapshotEnabled } = queryOptions(options);
2830
+ return createUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key: serializedKey, ...query }, serializedKey, snapshotEnabled);
1744
2831
  },
1745
2832
  watchRich(key, options) {
1746
- const { schema: _schema, ...subscriptionOptions } = options ?? {};
1747
- return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...subscriptionOptions }, key);
2833
+ const serializedKey = wireKey(key);
2834
+ const { query, snapshotEnabled } = queryOptions(options);
2835
+ return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key: serializedKey, ...query }, serializedKey, snapshotEnabled);
1748
2836
  },
1749
- async get(key) {
1750
- return storage.get(viewDef.view, key);
2837
+ async get(key, options) {
2838
+ return this.getSync(key, options) ?? null;
1751
2839
  },
1752
- getSync(key) {
1753
- return storage.getSync(viewDef.view, key);
2840
+ getSync(key, options) {
2841
+ const { query, snapshotEnabled } = queryOptions(options);
2842
+ const snapshot = subscriptionRegistry.getSnapshot({
2843
+ view: viewDef.view,
2844
+ key: wireKey(key),
2845
+ ...query,
2846
+ }, snapshotEnabled);
2847
+ return snapshot ? snapshot.data[0] ?? null : undefined;
1754
2848
  },
1755
2849
  };
1756
2850
  }
1757
2851
  function createTypedListView(viewDef, storage, subscriptionRegistry) {
1758
2852
  return {
1759
2853
  use(options) {
1760
- const { schema: _schema, ...subscriptionOptions } = options ?? {};
1761
- return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, ...subscriptionOptions }, options);
2854
+ const { query } = queryOptions(options);
2855
+ return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, ...query }, options);
1762
2856
  },
1763
2857
  watch(options) {
1764
- const { schema: _schema, ...subscriptionOptions } = options ?? {};
1765
- return createUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...subscriptionOptions });
2858
+ const { query, snapshotEnabled } = queryOptions(options);
2859
+ return createUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...query }, undefined, snapshotEnabled);
1766
2860
  },
1767
2861
  watchRich(options) {
1768
- const { schema: _schema, ...subscriptionOptions } = options ?? {};
1769
- return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...subscriptionOptions });
2862
+ const { query, snapshotEnabled } = queryOptions(options);
2863
+ return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...query }, undefined, snapshotEnabled);
1770
2864
  },
1771
- async get() {
1772
- return storage.getAll(viewDef.view);
2865
+ async get(options) {
2866
+ return this.getSync(options) ?? [];
1773
2867
  },
1774
- getSync() {
1775
- return storage.getAllSync(viewDef.view);
2868
+ getSync(options) {
2869
+ const { query, snapshotEnabled } = queryOptions(options);
2870
+ const snapshot = subscriptionRegistry.getSnapshot({ view: viewDef.view, ...query }, snapshotEnabled);
2871
+ return snapshot ? [...snapshot.data] : undefined;
1776
2872
  },
1777
2873
  };
1778
2874
  }
@@ -1869,7 +2965,38 @@ function decodeBase64(encoded) {
1869
2965
  if (bufferCtor) {
1870
2966
  return new Uint8Array(bufferCtor.from(encoded, 'base64'));
1871
2967
  }
1872
- throw new Error('No base64 decoder available in this environment');
2968
+ throw new Error('No base64 decoder available in this environment');
2969
+ }
2970
+ function decimalU64(value, field) {
2971
+ if (!/^\d+$/.test(value)) {
2972
+ throw new TypeError(`${field} must be a decimal u64 string`);
2973
+ }
2974
+ const parsed = BigInt(value);
2975
+ if (parsed > 18446744073709551615n) {
2976
+ throw new RangeError(`${field} exceeds u64`);
2977
+ }
2978
+ return parsed;
2979
+ }
2980
+ function serializeContextSlot(value) {
2981
+ if (typeof value === 'number') {
2982
+ if (!Number.isSafeInteger(value) || value < 0) {
2983
+ throw new RangeError('minContextSlot must be a non-negative safe integer or bigint');
2984
+ }
2985
+ return String(value);
2986
+ }
2987
+ if (value < 0n || value > 18446744073709551615n) {
2988
+ throw new RangeError('minContextSlot must fit in u64');
2989
+ }
2990
+ return value.toString();
2991
+ }
2992
+ function withContextSlot(input, options) {
2993
+ if (options?.minContextSlot === undefined) {
2994
+ return input;
2995
+ }
2996
+ return {
2997
+ ...input,
2998
+ minContextSlot: serializeContextSlot(options.minContextSlot),
2999
+ };
1873
3000
  }
1874
3001
  function deriveHttpEndpoint(wsUrl) {
1875
3002
  try {
@@ -1900,6 +3027,19 @@ function createChainClient(httpBaseUrl, fetchImpl) {
1900
3027
  const body = await parseReadResponse(response, path);
1901
3028
  return body.lamports;
1902
3029
  },
3030
+ async nativeBalance(address, options) {
3031
+ const path = '/chain/native-balance';
3032
+ const response = await fetchImpl(joinUrl(httpBaseUrl, path), {
3033
+ method: 'POST',
3034
+ headers: { 'content-type': 'application/json' },
3035
+ body: JSON.stringify(withContextSlot({ address }, options)),
3036
+ });
3037
+ const body = await parseReadResponse(response, path);
3038
+ return {
3039
+ lamports: decimalU64(body.lamports, 'lamports'),
3040
+ contextSlot: decimalU64(body.contextSlot, 'contextSlot'),
3041
+ };
3042
+ },
1903
3043
  async minimumBalanceForRentExemption(space) {
1904
3044
  const path = `/chain/rent-exemption/${encodeURIComponent(String(space))}`;
1905
3045
  const response = await fetchImpl(joinUrl(httpBaseUrl, path));
@@ -1936,18 +3076,469 @@ function createChainClient(httpBaseUrl, fetchImpl) {
1936
3076
  const response = await fetchImpl(joinUrl(httpBaseUrl, path));
1937
3077
  return parseReadResponse(response, path);
1938
3078
  },
1939
- async balance(input) {
3079
+ async balance(input, options) {
1940
3080
  const path = '/chain/balances';
1941
3081
  const response = await fetchImpl(joinUrl(httpBaseUrl, path), {
1942
3082
  method: 'POST',
1943
3083
  headers: { 'content-type': 'application/json' },
1944
- body: JSON.stringify(input),
3084
+ body: JSON.stringify(withContextSlot(input, options)),
1945
3085
  });
1946
- return parseReadResponse(response, path);
3086
+ const body = await parseReadResponse(response, path);
3087
+ return {
3088
+ ...body,
3089
+ contextSlot: decimalU64(body.contextSlot, 'contextSlot'),
3090
+ };
1947
3091
  },
1948
3092
  };
1949
3093
  }
1950
3094
 
3095
+ /*! noble-ed25519 - MIT License (c) 2019 Paul Miller (paulmillr.com) */
3096
+ /**
3097
+ * 4KB JS implementation of ed25519 EdDSA signatures.
3098
+ * Compliant with RFC8032, FIPS 186-5 & ZIP215.
3099
+ * @module
3100
+ * @example
3101
+ * ```js
3102
+ import * as ed from '@noble/ed25519';
3103
+ (async () => {
3104
+ const privKey = ed.utils.randomPrivateKey();
3105
+ const message = Uint8Array.from([0xab, 0xbc, 0xcd, 0xde]);
3106
+ const pubKey = await ed.getPublicKeyAsync(privKey); // Sync methods are also present
3107
+ const signature = await ed.signAsync(message, privKey);
3108
+ const isValid = await ed.verifyAsync(signature, message, pubKey);
3109
+ })();
3110
+ ```
3111
+ */
3112
+ /**
3113
+ * Curve params. ed25519 is twisted edwards curve. Equation is −x² + y² = -a + dx²y².
3114
+ * * P = `2n**255n - 19n` // field over which calculations are done
3115
+ * * N = `2n**252n + 27742317777372353535851937790883648493n` // group order, amount of curve points
3116
+ * * h = 8 // cofactor
3117
+ * * a = `Fp.create(BigInt(-1))` // equation param
3118
+ * * d = -121665/121666 a.k.a. `Fp.neg(121665 * Fp.inv(121666))` // equation param
3119
+ * * Gx, Gy are coordinates of Generator / base point
3120
+ */
3121
+ const ed25519_CURVE = {
3122
+ p: 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffedn,
3123
+ n: 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3edn,
3124
+ h: 8n,
3125
+ a: 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffecn,
3126
+ d: 0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3n,
3127
+ Gx: 0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51an,
3128
+ Gy: 0x6666666666666666666666666666666666666666666666666666666666666658n,
3129
+ };
3130
+ const { p: P, n: N, Gx, Gy, a: _a, d: _d } = ed25519_CURVE;
3131
+ const h = 8n; // cofactor
3132
+ const L = 32; // field / group byte length
3133
+ const L2 = 64;
3134
+ // Helpers and Precomputes sections are reused between libraries
3135
+ // ## Helpers
3136
+ // ----------
3137
+ // error helper, messes-up stack trace
3138
+ const err = (m = '') => {
3139
+ throw new Error(m);
3140
+ };
3141
+ const isBig = (n) => typeof n === 'bigint'; // is big integer
3142
+ const isStr = (s) => typeof s === 'string'; // is string
3143
+ const isBytes = (a) => a instanceof Uint8Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array');
3144
+ /** assert is Uint8Array (of specific length) */
3145
+ const abytes = (a, l) => !isBytes(a) || (typeof l === 'number' && l > 0 && a.length !== l)
3146
+ ? err('Uint8Array expected')
3147
+ : a;
3148
+ /** create Uint8Array */
3149
+ const u8n = (len) => new Uint8Array(len);
3150
+ const u8fr = (buf) => Uint8Array.from(buf);
3151
+ const padh = (n, pad) => n.toString(16).padStart(pad, '0');
3152
+ const bytesToHex = (b) => Array.from(abytes(b))
3153
+ .map((e) => padh(e, 2))
3154
+ .join('');
3155
+ const C = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 }; // ASCII characters
3156
+ const _ch = (ch) => {
3157
+ if (ch >= C._0 && ch <= C._9)
3158
+ return ch - C._0; // '2' => 50-48
3159
+ if (ch >= C.A && ch <= C.F)
3160
+ return ch - (C.A - 10); // 'B' => 66-(65-10)
3161
+ if (ch >= C.a && ch <= C.f)
3162
+ return ch - (C.a - 10); // 'b' => 98-(97-10)
3163
+ return;
3164
+ };
3165
+ const hexToBytes = (hex) => {
3166
+ const e = 'hex invalid';
3167
+ if (!isStr(hex))
3168
+ return err(e);
3169
+ const hl = hex.length;
3170
+ const al = hl / 2;
3171
+ if (hl % 2)
3172
+ return err(e);
3173
+ const array = u8n(al);
3174
+ for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) {
3175
+ // treat each char as ASCII
3176
+ const n1 = _ch(hex.charCodeAt(hi)); // parse first char, multiply it by 16
3177
+ const n2 = _ch(hex.charCodeAt(hi + 1)); // parse second char
3178
+ if (n1 === undefined || n2 === undefined)
3179
+ return err(e);
3180
+ array[ai] = n1 * 16 + n2; // example: 'A9' => 10*16 + 9
3181
+ }
3182
+ return array;
3183
+ };
3184
+ /** normalize hex or ui8a to ui8a */
3185
+ const toU8 = (a, len) => abytes(isStr(a) ? hexToBytes(a) : u8fr(abytes(a)), len);
3186
+ const big = BigInt;
3187
+ const arange = (n, min, max, msg = 'bad number: out of range') => isBig(n) && min <= n && n < max ? n : err(msg);
3188
+ /** modular division */
3189
+ const M = (a, b = P) => {
3190
+ const r = a % b;
3191
+ return r >= 0n ? r : b + r;
3192
+ };
3193
+ /** Modular inversion using eucledian GCD (non-CT). No negative exponent for now. */
3194
+ // prettier-ignore
3195
+ const invert = (num, md) => {
3196
+ if (num === 0n || md <= 0n)
3197
+ err('no inverse n=' + num + ' mod=' + md);
3198
+ let a = M(num, md), b = md, x = 0n, u = 1n;
3199
+ while (a !== 0n) {
3200
+ const q = b / a, r = b % a;
3201
+ const m = x - u * q;
3202
+ b = a, a = r, x = u, u = m;
3203
+ }
3204
+ return b === 1n ? M(x, md) : err('no inverse'); // b is gcd at this point
3205
+ };
3206
+ const apoint = (p) => (p instanceof Point ? p : err('Point expected'));
3207
+ // ## End of Helpers
3208
+ // -----------------
3209
+ const B256 = 2n ** 256n;
3210
+ /** Point in XYZT extended coordinates. */
3211
+ class Point {
3212
+ static BASE;
3213
+ static ZERO;
3214
+ ex;
3215
+ ey;
3216
+ ez;
3217
+ et;
3218
+ constructor(ex, ey, ez, et) {
3219
+ const max = B256;
3220
+ this.ex = arange(ex, 0n, max);
3221
+ this.ey = arange(ey, 0n, max);
3222
+ this.ez = arange(ez, 1n, max);
3223
+ this.et = arange(et, 0n, max);
3224
+ Object.freeze(this);
3225
+ }
3226
+ static fromAffine(p) {
3227
+ return new Point(p.x, p.y, 1n, M(p.x * p.y));
3228
+ }
3229
+ /** RFC8032 5.1.3: Uint8Array to Point. */
3230
+ static fromBytes(hex, zip215 = false) {
3231
+ const d = _d;
3232
+ // Copy array to not mess it up.
3233
+ const normed = u8fr(abytes(hex, L));
3234
+ // adjust first LE byte = last BE byte
3235
+ const lastByte = hex[31];
3236
+ normed[31] = lastByte & ~0x80;
3237
+ const y = bytesToNumLE(normed);
3238
+ // zip215=true: 0 <= y < 2^256
3239
+ // zip215=false, RFC8032: 0 <= y < 2^255-19
3240
+ const max = zip215 ? B256 : P;
3241
+ arange(y, 0n, max);
3242
+ const y2 = M(y * y); // y²
3243
+ const u = M(y2 - 1n); // u=y²-1
3244
+ const v = M(d * y2 + 1n); // v=dy²+1
3245
+ let { isValid, value: x } = uvRatio(u, v); // (uv³)(uv⁷)^(p-5)/8; square root
3246
+ if (!isValid)
3247
+ err('bad point: y not sqrt'); // not square root: bad point
3248
+ const isXOdd = (x & 1n) === 1n; // adjust sign of x coordinate
3249
+ const isLastByteOdd = (lastByte & 0x80) !== 0; // x_0, last bit
3250
+ if (!zip215 && x === 0n && isLastByteOdd)
3251
+ err('bad point: x==0, isLastByteOdd'); // x=0, x_0=1
3252
+ if (isLastByteOdd !== isXOdd)
3253
+ x = M(-x);
3254
+ return new Point(x, y, 1n, M(x * y)); // Z=1, T=xy
3255
+ }
3256
+ /** Checks if the point is valid and on-curve. */
3257
+ assertValidity() {
3258
+ const a = _a;
3259
+ const d = _d;
3260
+ const p = this;
3261
+ if (p.is0())
3262
+ throw new Error('bad point: ZERO'); // TODO: optimize, with vars below?
3263
+ // Equation in affine coordinates: ax² + y² = 1 + dx²y²
3264
+ // Equation in projective coordinates (X/Z, Y/Z, Z): (aX² + Y²)Z² = Z⁴ + dX²Y²
3265
+ const { ex: X, ey: Y, ez: Z, et: T } = p;
3266
+ const X2 = M(X * X); // X²
3267
+ const Y2 = M(Y * Y); // Y²
3268
+ const Z2 = M(Z * Z); // Z²
3269
+ const Z4 = M(Z2 * Z2); // Z⁴
3270
+ const aX2 = M(X2 * a); // aX²
3271
+ const left = M(Z2 * M(aX2 + Y2)); // (aX² + Y²)Z²
3272
+ const right = M(Z4 + M(d * M(X2 * Y2))); // Z⁴ + dX²Y²
3273
+ if (left !== right)
3274
+ throw new Error('bad point: equation left != right (1)');
3275
+ // In Extended coordinates we also have T, which is x*y=T/Z: check X*Y == Z*T
3276
+ const XY = M(X * Y);
3277
+ const ZT = M(Z * T);
3278
+ if (XY !== ZT)
3279
+ throw new Error('bad point: equation left != right (2)');
3280
+ return this;
3281
+ }
3282
+ /** Equality check: compare points P&Q. */
3283
+ equals(other) {
3284
+ const { ex: X1, ey: Y1, ez: Z1 } = this;
3285
+ const { ex: X2, ey: Y2, ez: Z2 } = apoint(other); // checks class equality
3286
+ const X1Z2 = M(X1 * Z2);
3287
+ const X2Z1 = M(X2 * Z1);
3288
+ const Y1Z2 = M(Y1 * Z2);
3289
+ const Y2Z1 = M(Y2 * Z1);
3290
+ return X1Z2 === X2Z1 && Y1Z2 === Y2Z1;
3291
+ }
3292
+ is0() {
3293
+ return this.equals(I);
3294
+ }
3295
+ /** Flip point over y coordinate. */
3296
+ negate() {
3297
+ return new Point(M(-this.ex), this.ey, this.ez, M(-this.et));
3298
+ }
3299
+ /** Point doubling. Complete formula. Cost: `4M + 4S + 1*a + 6add + 1*2`. */
3300
+ double() {
3301
+ const { ex: X1, ey: Y1, ez: Z1 } = this;
3302
+ const a = _a;
3303
+ // https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#doubling-dbl-2008-hwcd
3304
+ const A = M(X1 * X1);
3305
+ const B = M(Y1 * Y1);
3306
+ const C = M(2n * M(Z1 * Z1));
3307
+ const D = M(a * A);
3308
+ const x1y1 = X1 + Y1;
3309
+ const E = M(M(x1y1 * x1y1) - A - B);
3310
+ const G = D + B;
3311
+ const F = G - C;
3312
+ const H = D - B;
3313
+ const X3 = M(E * F);
3314
+ const Y3 = M(G * H);
3315
+ const T3 = M(E * H);
3316
+ const Z3 = M(F * G);
3317
+ return new Point(X3, Y3, Z3, T3);
3318
+ }
3319
+ /** Point addition. Complete formula. Cost: `8M + 1*k + 8add + 1*2`. */
3320
+ add(other) {
3321
+ const { ex: X1, ey: Y1, ez: Z1, et: T1 } = this;
3322
+ const { ex: X2, ey: Y2, ez: Z2, et: T2 } = apoint(other); // doesn't check if other on-curve
3323
+ const a = _a;
3324
+ const d = _d;
3325
+ // https://hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html#addition-add-2008-hwcd-3
3326
+ const A = M(X1 * X2);
3327
+ const B = M(Y1 * Y2);
3328
+ const C = M(T1 * d * T2);
3329
+ const D = M(Z1 * Z2);
3330
+ const E = M((X1 + Y1) * (X2 + Y2) - A - B);
3331
+ const F = M(D - C);
3332
+ const G = M(D + C);
3333
+ const H = M(B - a * A);
3334
+ const X3 = M(E * F);
3335
+ const Y3 = M(G * H);
3336
+ const T3 = M(E * H);
3337
+ const Z3 = M(F * G);
3338
+ return new Point(X3, Y3, Z3, T3);
3339
+ }
3340
+ /**
3341
+ * Point-by-scalar multiplication. Scalar must be in range 1 <= n < CURVE.n.
3342
+ * Uses {@link wNAF} for base point.
3343
+ * Uses fake point to mitigate side-channel leakage.
3344
+ * @param n scalar by which point is multiplied
3345
+ * @param safe safe mode guards against timing attacks; unsafe mode is faster
3346
+ */
3347
+ multiply(n, safe = true) {
3348
+ if (!safe && (n === 0n || this.is0()))
3349
+ return I;
3350
+ arange(n, 1n, N);
3351
+ if (n === 1n)
3352
+ return this;
3353
+ if (this.equals(G))
3354
+ return wNAF(n).p;
3355
+ // init result point & fake point
3356
+ let p = I;
3357
+ let f = G;
3358
+ for (let d = this; n > 0n; d = d.double(), n >>= 1n) {
3359
+ // if bit is present, add to point
3360
+ // if not present, add to fake, for timing safety
3361
+ if (n & 1n)
3362
+ p = p.add(d);
3363
+ else if (safe)
3364
+ f = f.add(d);
3365
+ }
3366
+ return p;
3367
+ }
3368
+ /** Convert point to 2d xy affine point. (X, Y, Z) ∋ (x=X/Z, y=Y/Z) */
3369
+ toAffine() {
3370
+ const { ex: x, ey: y, ez: z } = this;
3371
+ // fast-paths for ZERO point OR Z=1
3372
+ if (this.equals(I))
3373
+ return { x: 0n, y: 1n };
3374
+ const iz = invert(z, P);
3375
+ // (Z * Z^-1) must be 1, otherwise bad math
3376
+ if (M(z * iz) !== 1n)
3377
+ err('invalid inverse');
3378
+ // x = X*Z^-1; y = Y*Z^-1
3379
+ return { x: M(x * iz), y: M(y * iz) };
3380
+ }
3381
+ toBytes() {
3382
+ const { x, y } = this.assertValidity().toAffine();
3383
+ const b = numTo32bLE(y);
3384
+ // store sign in first LE byte
3385
+ b[31] |= x & 1n ? 0x80 : 0;
3386
+ return b;
3387
+ }
3388
+ toHex() {
3389
+ return bytesToHex(this.toBytes());
3390
+ } // encode to hex string
3391
+ clearCofactor() {
3392
+ return this.multiply(big(h), false);
3393
+ }
3394
+ isSmallOrder() {
3395
+ return this.clearCofactor().is0();
3396
+ }
3397
+ isTorsionFree() {
3398
+ // multiply by big number CURVE.n
3399
+ let p = this.multiply(N / 2n, false).double(); // ensures the point is not "bad".
3400
+ if (N % 2n)
3401
+ p = p.add(this); // P^(N+1) // P*N == (P*(N/2))*2+P
3402
+ return p.is0();
3403
+ }
3404
+ static fromHex(hex, zip215) {
3405
+ return Point.fromBytes(toU8(hex), zip215);
3406
+ }
3407
+ get x() {
3408
+ return this.toAffine().x;
3409
+ }
3410
+ get y() {
3411
+ return this.toAffine().y;
3412
+ }
3413
+ toRawBytes() {
3414
+ return this.toBytes();
3415
+ }
3416
+ }
3417
+ /** Generator / base point */
3418
+ const G = new Point(Gx, Gy, 1n, M(Gx * Gy));
3419
+ /** Identity / zero point */
3420
+ const I = new Point(0n, 1n, 1n, 0n);
3421
+ // Static aliases
3422
+ Point.BASE = G;
3423
+ Point.ZERO = I;
3424
+ const numTo32bLE = (num) => hexToBytes(padh(arange(num, 0n, B256), L2)).reverse();
3425
+ const bytesToNumLE = (b) => big('0x' + bytesToHex(u8fr(abytes(b)).reverse()));
3426
+ const pow2 = (x, power) => {
3427
+ // pow2(x, 4) == x^(2^4)
3428
+ let r = x;
3429
+ while (power-- > 0n) {
3430
+ r *= r;
3431
+ r %= P;
3432
+ }
3433
+ return r;
3434
+ };
3435
+ // prettier-ignore
3436
+ const pow_2_252_3 = (x) => {
3437
+ const x2 = (x * x) % P; // x^2, bits 1
3438
+ const b2 = (x2 * x) % P; // x^3, bits 11
3439
+ const b4 = (pow2(b2, 2n) * b2) % P; // x^(2^4-1), bits 1111
3440
+ const b5 = (pow2(b4, 1n) * x) % P; // x^(2^5-1), bits 11111
3441
+ const b10 = (pow2(b5, 5n) * b5) % P; // x^(2^10)
3442
+ const b20 = (pow2(b10, 10n) * b10) % P; // x^(2^20)
3443
+ const b40 = (pow2(b20, 20n) * b20) % P; // x^(2^40)
3444
+ const b80 = (pow2(b40, 40n) * b40) % P; // x^(2^80)
3445
+ const b160 = (pow2(b80, 80n) * b80) % P; // x^(2^160)
3446
+ const b240 = (pow2(b160, 80n) * b80) % P; // x^(2^240)
3447
+ const b250 = (pow2(b240, 10n) * b10) % P; // x^(2^250)
3448
+ const pow_p_5_8 = (pow2(b250, 2n) * x) % P; // < To pow to (p+3)/8, multiply it by x.
3449
+ return { pow_p_5_8, b2 };
3450
+ };
3451
+ const RM1 = 0x2b8324804fc1df0b2b4d00993dfbd7a72f431806ad2fe478c4ee1b274a0ea0b0n; // √-1
3452
+ // for sqrt comp
3453
+ // prettier-ignore
3454
+ const uvRatio = (u, v) => {
3455
+ const v3 = M(v * v * v); // v³
3456
+ const v7 = M(v3 * v3 * v); // v⁷
3457
+ const pow = pow_2_252_3(u * v7).pow_p_5_8; // (uv⁷)^(p-5)/8
3458
+ let x = M(u * v3 * pow); // (uv³)(uv⁷)^(p-5)/8
3459
+ const vx2 = M(v * x * x); // vx²
3460
+ const root1 = x; // First root candidate
3461
+ const root2 = M(x * RM1); // Second root candidate; RM1 is √-1
3462
+ const useRoot1 = vx2 === u; // If vx² = u (mod p), x is a square root
3463
+ const useRoot2 = vx2 === M(-u); // If vx² = -u, set x <-- x * 2^((p-1)/4)
3464
+ const noRoot = vx2 === M(-u * RM1); // There is no valid root, vx² = -u√-1
3465
+ if (useRoot1)
3466
+ x = root1;
3467
+ if (useRoot2 || noRoot)
3468
+ x = root2; // We return root2 anyway, for const-time
3469
+ if ((M(x) & 1n) === 1n)
3470
+ x = M(-x); // edIsNegative
3471
+ return { isValid: useRoot1 || useRoot2, value: x };
3472
+ };
3473
+ // ## Precomputes
3474
+ // --------------
3475
+ const W = 8; // W is window size
3476
+ const scalarBits = 256;
3477
+ const pwindows = Math.ceil(scalarBits / W) + 1; // 33 for W=8
3478
+ const pwindowSize = 2 ** (W - 1); // 128 for W=8
3479
+ const precompute = () => {
3480
+ const points = [];
3481
+ let p = G;
3482
+ let b = p;
3483
+ for (let w = 0; w < pwindows; w++) {
3484
+ b = p;
3485
+ points.push(b);
3486
+ for (let i = 1; i < pwindowSize; i++) {
3487
+ b = b.add(p);
3488
+ points.push(b);
3489
+ } // i=1, bc we skip 0
3490
+ p = b.double();
3491
+ }
3492
+ return points;
3493
+ };
3494
+ let Gpows = undefined; // precomputes for base point G
3495
+ // const-time negate
3496
+ const ctneg = (cnd, p) => {
3497
+ const n = p.negate();
3498
+ return cnd ? n : p;
3499
+ };
3500
+ /**
3501
+ * Precomputes give 12x faster getPublicKey(), 10x sign(), 2x verify() by
3502
+ * caching multiples of G (base point). Cache is stored in 32MB of RAM.
3503
+ * Any time `G.multiply` is done, precomputes are used.
3504
+ * Not used for getSharedSecret, which instead multiplies random pubkey `P.multiply`.
3505
+ *
3506
+ * w-ary non-adjacent form (wNAF) precomputation method is 10% slower than windowed method,
3507
+ * but takes 2x less RAM. RAM reduction is possible by utilizing `.subtract`.
3508
+ *
3509
+ * !! Precomputes can be disabled by commenting-out call of the wNAF() inside Point#multiply().
3510
+ */
3511
+ const wNAF = (n) => {
3512
+ const comp = Gpows || (Gpows = precompute());
3513
+ let p = I;
3514
+ let f = G; // f must be G, or could become I in the end
3515
+ const pow_2_w = 2 ** W; // 256 for W=8
3516
+ const maxNum = pow_2_w; // 256 for W=8
3517
+ const mask = big(pow_2_w - 1); // 255 for W=8 == mask 0b11111111
3518
+ const shiftBy = big(W); // 8 for W=8
3519
+ for (let w = 0; w < pwindows; w++) {
3520
+ let wbits = Number(n & mask); // extract W bits.
3521
+ n >>= shiftBy; // shift number by W bits.
3522
+ if (wbits > pwindowSize) {
3523
+ wbits -= maxNum;
3524
+ n += 1n;
3525
+ } // split if bits > max: +224 => 256-32
3526
+ const off = w * pwindowSize;
3527
+ const offF = off; // offsets, evaluate both
3528
+ const offP = off + Math.abs(wbits) - 1;
3529
+ const isEven = w % 2 !== 0; // conditions, evaluate both
3530
+ const isNeg = wbits < 0;
3531
+ if (wbits === 0) {
3532
+ // off == I: can't add it. Adding random offF instead.
3533
+ f = f.add(ctneg(isEven, comp[offF])); // bits are 0: add garbage to fake point
3534
+ }
3535
+ else {
3536
+ p = p.add(ctneg(isNeg, comp[offP])); // bits are 1: add to result point
3537
+ }
3538
+ }
3539
+ return { p, f }; // return both real and fake points for JIT
3540
+ };
3541
+
1951
3542
  /**
1952
3543
  * PDA (Program Derived Address) derivation utilities.
1953
3544
  *
@@ -2899,6 +4490,19 @@ function serializeArray(values, elementType, length) {
2899
4490
  /**
2900
4491
  * Parses and handles instruction errors.
2901
4492
  */
4493
+ /**
4494
+ * Structured transaction failure thrown by adapters and the core executor.
4495
+ */
4496
+ class TransactionExecutionError extends Error {
4497
+ constructor(outcome, message) {
4498
+ super(message ?? errorMessage(outcome.cause, defaultOutcomeMessage(outcome)));
4499
+ this.name = 'TransactionExecutionError';
4500
+ this.outcome = outcome;
4501
+ this.cause = outcome.cause;
4502
+ this.signature = 'signature' in outcome ? outcome.signature : undefined;
4503
+ this.slot = 'slot' in outcome ? outcome.slot : undefined;
4504
+ }
4505
+ }
2902
4506
  /**
2903
4507
  * Parses an error returned from a Solana transaction.
2904
4508
  *
@@ -2907,44 +4511,84 @@ function serializeArray(values, elementType, length) {
2907
4511
  * @returns Parsed program error or null if not a program error
2908
4512
  */
2909
4513
  function parseInstructionError(error, errorMetadata) {
2910
- if (!error) {
2911
- return null;
4514
+ return parseInstructionErrorMatch(error, errorMetadata)?.programError ?? null;
4515
+ }
4516
+ function extractErrorCodes(error, seen, results) {
4517
+ if (typeof error !== 'object' || error === null) {
4518
+ return;
2912
4519
  }
2913
- const errorCode = extractErrorCode(error);
2914
- if (errorCode === null) {
2915
- return null;
4520
+ if (seen.has(error)) {
4521
+ return;
2916
4522
  }
2917
- const metadata = errorMetadata.find(e => e.code === errorCode);
2918
- if (metadata) {
2919
- return {
2920
- code: metadata.code,
2921
- name: metadata.name,
2922
- message: metadata.msg,
2923
- };
4523
+ seen.add(error);
4524
+ if (error instanceof InstructionError && error.programError) {
4525
+ results.push({ code: error.programError.code, source: 'program-error' });
4526
+ return;
2924
4527
  }
2925
- return {
2926
- code: errorCode,
2927
- name: `CustomError${errorCode}`,
2928
- message: `Unknown error with code ${errorCode}`,
2929
- };
2930
- }
2931
- function extractErrorCode(error) {
2932
- if (typeof error !== 'object' || error === null) {
2933
- return null;
4528
+ if (error instanceof TransactionExecutionError) {
4529
+ const outcomeCode = error.outcome.status === 'chain-failed'
4530
+ ? error.outcome.programError?.code
4531
+ : undefined;
4532
+ if (outcomeCode !== undefined) {
4533
+ results.push({ code: outcomeCode, source: 'program-error' });
4534
+ return;
4535
+ }
2934
4536
  }
2935
4537
  const errorObj = error;
2936
4538
  // Check for InstructionError format
2937
- if (errorObj.InstructionError) {
4539
+ if (Array.isArray(errorObj.InstructionError)) {
2938
4540
  const instructionError = errorObj.InstructionError;
2939
- if (instructionError[1]?.Custom !== undefined) {
2940
- return instructionError[1].Custom;
4541
+ const detail = instructionError[1];
4542
+ if (typeof detail === 'object' && detail !== null) {
4543
+ const custom = detail.Custom;
4544
+ if (typeof custom === 'number') {
4545
+ results.push({ code: custom, source: 'instruction-error' });
4546
+ }
4547
+ }
4548
+ }
4549
+ const programError = errorObj.programError;
4550
+ if (typeof programError === 'object' && programError !== null) {
4551
+ const code = programError.code;
4552
+ if (typeof code === 'number') {
4553
+ results.push({ code, source: 'program-error' });
2941
4554
  }
2942
4555
  }
2943
- // Check for direct code
2944
4556
  if (typeof errorObj.code === 'number') {
2945
- return errorObj.code;
4557
+ results.push({ code: errorObj.code, source: 'direct-code' });
2946
4558
  }
2947
- return null;
4559
+ for (const key of ['cause', 'error', 'err', 'value', 'data', 'outcome', 'transactionError']) {
4560
+ extractErrorCodes(errorObj[key], seen, results);
4561
+ }
4562
+ }
4563
+ function parseInstructionErrorMatch(error, errorMetadata) {
4564
+ if (!error) {
4565
+ return null;
4566
+ }
4567
+ const candidates = [];
4568
+ extractErrorCodes(error, new Set(), candidates);
4569
+ const selected = candidates.find((candidate) => candidate.source === 'instruction-error')
4570
+ ?? candidates.find((candidate) => candidate.source === 'program-error')
4571
+ ?? candidates.find((candidate) => errorMetadata.some((metadata) => metadata.code === candidate.code))
4572
+ ?? candidates[0];
4573
+ if (!selected) {
4574
+ return null;
4575
+ }
4576
+ const metadata = errorMetadata.find((entry) => entry.code === selected.code);
4577
+ const programError = metadata
4578
+ ? {
4579
+ code: metadata.code,
4580
+ name: metadata.name,
4581
+ message: metadata.msg,
4582
+ }
4583
+ : {
4584
+ code: selected.code,
4585
+ name: `CustomError${selected.code}`,
4586
+ message: `Unknown error with code ${selected.code}`,
4587
+ };
4588
+ return {
4589
+ programError,
4590
+ deterministic: selected.source !== 'direct-code' || metadata !== undefined,
4591
+ };
2948
4592
  }
2949
4593
  /**
2950
4594
  * Formats an error for display.
@@ -2960,12 +4604,185 @@ function formatProgramError(error) {
2960
4604
  * could be parsed against the handler's IDL error definitions.
2961
4605
  */
2962
4606
  class InstructionError extends Error {
2963
- constructor(message, programError, cause) {
4607
+ constructor(message, programError, cause, outcome) {
2964
4608
  super(message);
2965
4609
  this.name = 'InstructionError';
2966
4610
  this.programError = programError;
2967
- this.cause = cause;
4611
+ if (outcome) {
4612
+ this.outcome = outcome;
4613
+ }
4614
+ else {
4615
+ const context = getTransactionFailureOutcome(cause);
4616
+ this.outcome = {
4617
+ status: 'chain-failed',
4618
+ phase: 'chain',
4619
+ signature: context && 'signature' in context ? context.signature : undefined,
4620
+ slot: context && 'slot' in context ? context.slot : undefined,
4621
+ programError: programError ?? undefined,
4622
+ cause: context?.cause ?? cause,
4623
+ };
4624
+ }
4625
+ this.cause = this.outcome.cause;
4626
+ this.signature = this.outcome.signature;
4627
+ this.slot = this.outcome.slot;
4628
+ }
4629
+ }
4630
+ function defaultOutcomeMessage(outcome) {
4631
+ switch (outcome.status) {
4632
+ case 'not-submitted':
4633
+ return `Transaction was not submitted during ${outcome.phase}`;
4634
+ case 'submitted-unknown':
4635
+ return `Transaction ${outcome.signature} was submitted but its status is unknown`;
4636
+ case 'chain-failed':
4637
+ return outcome.signature
4638
+ ? `Transaction ${outcome.signature} failed on chain`
4639
+ : 'Transaction failed on chain';
4640
+ }
4641
+ }
4642
+ function errorMessage(cause, fallback) {
4643
+ return cause instanceof Error && cause.message ? cause.message : fallback;
4644
+ }
4645
+ function isFailureOutcome(value) {
4646
+ if (!value || typeof value !== 'object') {
4647
+ return false;
4648
+ }
4649
+ const candidate = value;
4650
+ return ((candidate.status === 'not-submitted'
4651
+ || candidate.status === 'submitted-unknown'
4652
+ || candidate.status === 'chain-failed')
4653
+ && typeof candidate.phase === 'string'
4654
+ && 'cause' in candidate);
4655
+ }
4656
+ /** Find a structured transaction failure through nested error causes. */
4657
+ function getTransactionFailureOutcome(error) {
4658
+ const seen = new Set();
4659
+ let current = error;
4660
+ while (typeof current === 'object' && current !== null && !seen.has(current)) {
4661
+ seen.add(current);
4662
+ if (current instanceof TransactionExecutionError || current instanceof InstructionError) {
4663
+ return current.outcome;
4664
+ }
4665
+ const candidate = current;
4666
+ if (isFailureOutcome(candidate.outcome)) {
4667
+ return candidate.outcome;
4668
+ }
4669
+ current = candidate.cause;
4670
+ }
4671
+ return null;
4672
+ }
4673
+ function extractTransactionContext(error) {
4674
+ const seen = new Set();
4675
+ let current = error;
4676
+ let signature;
4677
+ let slot;
4678
+ while (typeof current === 'object' && current !== null && !seen.has(current)) {
4679
+ seen.add(current);
4680
+ const candidate = current;
4681
+ if (!signature && typeof candidate.signature === 'string') {
4682
+ signature = candidate.signature;
4683
+ }
4684
+ if (slot === undefined && typeof candidate.slot === 'number') {
4685
+ slot = candidate.slot;
4686
+ }
4687
+ if (isFailureOutcome(candidate.outcome)) {
4688
+ if (!signature && 'signature' in candidate.outcome) {
4689
+ signature = candidate.outcome.signature;
4690
+ }
4691
+ if (slot === undefined && 'slot' in candidate.outcome) {
4692
+ slot = candidate.outcome.slot;
4693
+ }
4694
+ }
4695
+ current = candidate.cause;
4696
+ }
4697
+ return { signature, slot };
4698
+ }
4699
+ function isWalletRejection(error) {
4700
+ const seen = new Set();
4701
+ let current = error;
4702
+ while (typeof current === 'object' && current !== null && !seen.has(current)) {
4703
+ seen.add(current);
4704
+ const candidate = current;
4705
+ if (candidate.code === 4001
4706
+ || candidate.code === '4001'
4707
+ || candidate.code === 'ACTION_REJECTED') {
4708
+ return true;
4709
+ }
4710
+ const name = String(candidate.name ?? '');
4711
+ if (/^(UserRejectedRequestError|UserRejectError|WalletRequestRejectedError)$/.test(name)) {
4712
+ return true;
4713
+ }
4714
+ const message = String(candidate.message ?? '').trim();
4715
+ if (/^(?:the )?user (?:rejected|declined|denied)(?: the)? (?:request|transaction|signature request|wallet request)[.!]?$/i.test(message)
4716
+ || /^(?:request|transaction) (?:was )?(?:rejected|declined|denied) by (?:the )?user[.!]?$/i.test(message)) {
4717
+ return true;
4718
+ }
4719
+ current = candidate.cause ?? candidate.error;
4720
+ }
4721
+ return false;
4722
+ }
4723
+ /**
4724
+ * Normalize an adapter/host exception without retrying or resubmitting.
4725
+ */
4726
+ function normalizeTransactionError(cause, errorMetadata = [], fallbackPhase = 'send') {
4727
+ if (cause instanceof InstructionError) {
4728
+ return cause;
4729
+ }
4730
+ const existingOutcome = getTransactionFailureOutcome(cause);
4731
+ const instructionMatch = parseInstructionErrorMatch(cause, errorMetadata);
4732
+ const asInstructionError = (programError) => {
4733
+ const context = extractTransactionContext(cause);
4734
+ const originalCause = existingOutcome?.cause ?? cause;
4735
+ const outcome = {
4736
+ status: 'chain-failed',
4737
+ phase: existingOutcome?.status === 'chain-failed'
4738
+ ? existingOutcome.phase
4739
+ : 'chain',
4740
+ signature: context.signature,
4741
+ slot: context.slot,
4742
+ programError,
4743
+ cause: originalCause,
4744
+ };
4745
+ return new InstructionError(formatProgramError(programError), programError, originalCause, outcome);
4746
+ };
4747
+ if (instructionMatch?.deterministic) {
4748
+ return asInstructionError(instructionMatch.programError);
4749
+ }
4750
+ if (existingOutcome) {
4751
+ return cause instanceof TransactionExecutionError
4752
+ ? cause
4753
+ : new TransactionExecutionError(existingOutcome);
4754
+ }
4755
+ if (!existingOutcome && isWalletRejection(cause)) {
4756
+ return new TransactionExecutionError({
4757
+ status: 'not-submitted',
4758
+ phase: 'wallet',
4759
+ cause,
4760
+ });
4761
+ }
4762
+ if (instructionMatch) {
4763
+ return asInstructionError(instructionMatch.programError);
4764
+ }
4765
+ if (cause instanceof TransactionExecutionError) {
4766
+ return cause;
4767
+ }
4768
+ if (existingOutcome) {
4769
+ return new TransactionExecutionError(existingOutcome);
2968
4770
  }
4771
+ const context = extractTransactionContext(cause);
4772
+ if (context.signature) {
4773
+ return new TransactionExecutionError({
4774
+ status: 'submitted-unknown',
4775
+ phase: 'confirmation',
4776
+ signature: context.signature,
4777
+ slot: context.slot,
4778
+ cause,
4779
+ });
4780
+ }
4781
+ return new TransactionExecutionError({
4782
+ status: 'not-submitted',
4783
+ phase: fallbackPhase,
4784
+ cause,
4785
+ });
2969
4786
  }
2970
4787
 
2971
4788
  /**
@@ -3076,9 +4893,24 @@ function buildInstruction(handler, params, options = {}) {
3076
4893
  * {@link InstructionError}.
3077
4894
  */
3078
4895
  async function executeInstruction(handler, params, options = {}) {
3079
- const instruction = buildInstruction(handler, params, options);
4896
+ let instruction;
4897
+ try {
4898
+ instruction = buildInstruction(handler, params, options);
4899
+ }
4900
+ catch (cause) {
4901
+ throw new TransactionExecutionError({
4902
+ status: 'not-submitted',
4903
+ phase: 'build',
4904
+ cause,
4905
+ });
4906
+ }
3080
4907
  if (!options.wallet) {
3081
- throw new Error('Wallet required to sign and send transaction');
4908
+ const cause = new Error('Wallet required to sign and send transaction');
4909
+ throw new TransactionExecutionError({
4910
+ status: 'not-submitted',
4911
+ phase: 'wallet',
4912
+ cause,
4913
+ });
3082
4914
  }
3083
4915
  const sendOptions = {
3084
4916
  ...options.send,
@@ -3091,11 +4923,7 @@ async function executeInstruction(handler, params, options = {}) {
3091
4923
  return { signature: result.signature, slot: result.slot };
3092
4924
  }
3093
4925
  catch (err) {
3094
- const programError = parseInstructionError(err, handler.errors);
3095
- if (programError) {
3096
- throw new InstructionError(`${programError.name} (${programError.code}): ${programError.message}`, programError, err);
3097
- }
3098
- throw err;
4926
+ throw normalizeTransactionError(err, handler.errors);
3099
4927
  }
3100
4928
  }
3101
4929
  /**
@@ -3199,6 +5027,7 @@ function mergeOperations(base, extension) {
3199
5027
  function extendProgram(program, extensions) {
3200
5028
  const base = program;
3201
5029
  const extended = { ...program };
5030
+ delete extended.definitionHash;
3202
5031
  for (const key of ['pdas', 'accounts', 'queries', 'addresses', 'constants', 'defaults', 'math']) {
3203
5032
  const extensionValue = extensions[key];
3204
5033
  if (extensionValue !== undefined) {
@@ -3259,6 +5088,9 @@ function extendStack(stack, extensions) {
3259
5088
  if (baseRuntime || extensions.createRead || extensions.createFlows) {
3260
5089
  Object.defineProperty(extended, STACK_RUNTIME_EXTENSIONS, {
3261
5090
  value: {
5091
+ readArgCounts: baseRuntime?.readArgCounts && extensions.readArgCounts
5092
+ ? mergeNamespace(baseRuntime.readArgCounts, extensions.readArgCounts)
5093
+ : extensions.readArgCounts ?? baseRuntime?.readArgCounts,
3262
5094
  createRead: baseRuntime?.createRead && extensions.createRead
3263
5095
  ? (client) => mergeNamespace(baseRuntime.createRead(client), extensions.createRead(client))
3264
5096
  : extensions.createRead ?? baseRuntime?.createRead,
@@ -3467,19 +5299,69 @@ function validateTransactionSigners(transaction, host, options) {
3467
5299
  throw new Error(`Missing signer(s) for ${transaction.name}: ${missing.join(', ')}`);
3468
5300
  }
3469
5301
  }
5302
+ class OperationCallbackError extends Error {
5303
+ constructor(input) {
5304
+ super(`Operation '${input.operation.name}' ${input.phase} callback failed for transaction ${input.transactionIndex + 1} (${input.transaction.name})`);
5305
+ this.name = 'OperationCallbackError';
5306
+ this.phase = input.phase;
5307
+ this.operation = input.operation;
5308
+ this.transaction = input.transaction;
5309
+ this.transactionIndex = input.transactionIndex;
5310
+ this.receipt = input.receipt;
5311
+ this.cause = input.cause;
5312
+ }
5313
+ }
3470
5314
  class OperationExecutionError extends Error {
3471
5315
  constructor(input) {
3472
- super(`Operation '${input.operation.name}' failed at transaction ${input.failedTransactionIndex + 1} (${input.failedTransaction.name})`);
5316
+ const context = `Operation '${input.operation.name}' failed at transaction ${input.failedTransactionIndex + 1} (${input.failedTransaction.name})`;
5317
+ const detail = input.cause instanceof Error
5318
+ ? input.cause.message
5319
+ : typeof input.cause === 'string' ? input.cause : '';
5320
+ super(detail ? `${context}: ${detail}` : context);
3473
5321
  this.name = 'OperationExecutionError';
3474
5322
  this.operation = input.operation;
3475
5323
  this.failedTransaction = input.failedTransaction;
3476
5324
  this.failedTransactionIndex = input.failedTransactionIndex;
3477
5325
  this.completedReceipts = [...input.completedReceipts];
5326
+ this.callbackErrors = [...(input.callbackErrors ?? [])];
5327
+ this.outcome = getTransactionFailureOutcome(input.cause) ?? input.outcome ?? {
5328
+ status: 'not-submitted',
5329
+ phase: 'send',
5330
+ cause: input.cause,
5331
+ };
5332
+ this.signature = 'signature' in this.outcome ? this.outcome.signature : undefined;
5333
+ this.slot = 'slot' in this.outcome ? this.outcome.slot : undefined;
3478
5334
  this.cause = input.cause;
3479
5335
  }
3480
5336
  }
5337
+ async function runOperationCallback(input, callbackErrors, onCallbackError) {
5338
+ if (!input.callback) {
5339
+ return;
5340
+ }
5341
+ try {
5342
+ await input.callback();
5343
+ }
5344
+ catch (cause) {
5345
+ const error = new OperationCallbackError({
5346
+ phase: input.phase,
5347
+ operation: input.operation,
5348
+ transaction: input.transaction,
5349
+ transactionIndex: input.transactionIndex,
5350
+ receipt: input.receipt,
5351
+ cause,
5352
+ });
5353
+ callbackErrors.push(error);
5354
+ try {
5355
+ await onCallbackError?.(error);
5356
+ }
5357
+ catch {
5358
+ // The callback-error observer is also observational and must not alter execution.
5359
+ }
5360
+ }
5361
+ }
3481
5362
  async function executePreparedOperation(host, operation, options = {}) {
3482
5363
  const receipts = [];
5364
+ const callbackErrors = [];
3483
5365
  const signers = [
3484
5366
  ...new Set([
3485
5367
  ...(options.signerRegistry?.values() ?? []),
@@ -3489,37 +5371,71 @@ async function executePreparedOperation(host, operation, options = {}) {
3489
5371
  for (const [transactionIndex, transaction] of operation.plan.transactions.entries()) {
3490
5372
  try {
3491
5373
  validateTransactionSigners(transaction, host, options);
3492
- await options.onTransactionStart?.({ operation, transaction, transactionIndex });
3493
- const result = await host.transaction(transaction.instructions, {
5374
+ }
5375
+ catch (cause) {
5376
+ throw new OperationExecutionError({
5377
+ operation,
5378
+ failedTransaction: transaction,
5379
+ failedTransactionIndex: transactionIndex,
5380
+ completedReceipts: receipts,
5381
+ callbackErrors,
5382
+ outcome: {
5383
+ status: 'not-submitted',
5384
+ phase: 'build',
5385
+ cause,
5386
+ },
5387
+ cause,
5388
+ });
5389
+ }
5390
+ const startEvent = { operation, transaction, transactionIndex };
5391
+ await runOperationCallback({
5392
+ phase: 'transaction-start',
5393
+ ...startEvent,
5394
+ callback: options.onTransactionStart
5395
+ ? () => options.onTransactionStart(startEvent)
5396
+ : undefined,
5397
+ }, callbackErrors, options.onCallbackError);
5398
+ let result;
5399
+ try {
5400
+ result = await host.transaction(transaction.instructions, {
3494
5401
  wallet: options.wallet,
3495
5402
  send: options.send,
3496
5403
  errors: [...transaction.errors],
3497
5404
  signers: signers.length > 0 ? signers : undefined,
3498
- });
3499
- const receipt = {
3500
- transactionIndex,
3501
- transactionName: transaction.name,
3502
- signature: result.signature,
3503
- slot: result.slot,
3504
- };
3505
- receipts.push(receipt);
3506
- await options.onTransactionSuccess?.({
3507
- operation,
3508
- transaction,
3509
- transactionIndex,
3510
- receipt,
5405
+ transactionTransport: options.transactionTransport,
3511
5406
  });
3512
5407
  }
3513
5408
  catch (cause) {
5409
+ const normalized = normalizeTransactionError(cause, transaction.errors);
3514
5410
  throw new OperationExecutionError({
3515
5411
  operation,
3516
5412
  failedTransaction: transaction,
3517
5413
  failedTransactionIndex: transactionIndex,
3518
5414
  completedReceipts: receipts,
3519
- cause,
5415
+ callbackErrors,
5416
+ outcome: getTransactionFailureOutcome(normalized) ?? undefined,
5417
+ cause: normalized,
3520
5418
  });
3521
5419
  }
5420
+ const receipt = {
5421
+ transactionIndex,
5422
+ transactionName: transaction.name,
5423
+ signature: result.signature,
5424
+ slot: result.slot,
5425
+ };
5426
+ receipts.push(receipt);
5427
+ const successEvent = { operation, transaction, transactionIndex, receipt };
5428
+ await runOperationCallback({
5429
+ phase: 'transaction-success',
5430
+ ...successEvent,
5431
+ callback: options.onTransactionSuccess
5432
+ ? () => options.onTransactionSuccess(successEvent)
5433
+ : undefined,
5434
+ }, callbackErrors, options.onCallbackError);
3522
5435
  }
5436
+ const callbackErrorResult = callbackErrors.length > 0
5437
+ ? { callbackErrors: [...callbackErrors] }
5438
+ : {};
3523
5439
  if (operation.kind === 'flow') {
3524
5440
  return {
3525
5441
  kind: 'flow',
@@ -3527,6 +5443,7 @@ async function executePreparedOperation(host, operation, options = {}) {
3527
5443
  artifacts: operation.artifacts,
3528
5444
  signatures: nonEmpty(receipts.map((receipt) => receipt.signature), `Operation '${operation.name}' signatures`),
3529
5445
  transactions: nonEmpty(receipts, `Operation '${operation.name}' receipts`),
5446
+ ...callbackErrorResult,
3530
5447
  };
3531
5448
  }
3532
5449
  return {
@@ -3535,8 +5452,25 @@ async function executePreparedOperation(host, operation, options = {}) {
3535
5452
  artifacts: operation.artifacts,
3536
5453
  signatures: [receipts[0].signature],
3537
5454
  transaction: receipts[0],
5455
+ ...callbackErrorResult,
3538
5456
  };
3539
5457
  }
5458
+ /**
5459
+ * Recursively unwrap operation context while retaining structured transaction outcomes.
5460
+ */
5461
+ function unwrapOperationExecutionError(error) {
5462
+ if (error instanceof InstructionError) {
5463
+ return error;
5464
+ }
5465
+ if (error instanceof TransactionExecutionError) {
5466
+ return error.outcome;
5467
+ }
5468
+ if (error instanceof OperationExecutionError) {
5469
+ const underlying = unwrapOperationExecutionError(error.cause);
5470
+ return underlying instanceof InstructionError ? underlying : error.outcome;
5471
+ }
5472
+ return error;
5473
+ }
3540
5474
  function convertToJsonValue(value, ancestors) {
3541
5475
  if (value === null || typeof value === 'string' || typeof value === 'boolean') {
3542
5476
  return value;
@@ -3619,6 +5553,31 @@ function describePreparedOperation(operation) {
3619
5553
  })),
3620
5554
  };
3621
5555
  }
5556
+ /**
5557
+ * Inspect one prepared instruction/transaction without signing or submission.
5558
+ * Multi-transaction flows are intentionally unsupported.
5559
+ */
5560
+ async function inspectPreparedOperation(wallet, operation, options, context) {
5561
+ if (operation.plan.transactions.length !== 1) {
5562
+ throw new Error(`Cannot inspect operation '${operation.name}': multi-transaction operation inspection is not supported`);
5563
+ }
5564
+ if (operation.kind === 'flow') {
5565
+ throw new Error(`Cannot inspect flow '${operation.name}': flow inspection is not supported`);
5566
+ }
5567
+ if (!wallet?.inspectTransaction) {
5568
+ throw new Error('Wallet adapter does not support unsigned transaction inspection');
5569
+ }
5570
+ const transaction = operation.plan.transactions[0];
5571
+ const description = describePreparedOperation(operation);
5572
+ const inspection = context
5573
+ ? await wallet.inspectTransaction(transaction.instructions, options, context)
5574
+ : await wallet.inspectTransaction(transaction.instructions, options);
5575
+ return {
5576
+ description,
5577
+ transaction: inspection,
5578
+ programError: parseInstructionError(inspection.error ?? inspection, transaction.errors),
5579
+ };
5580
+ }
3622
5581
  function formatPreparedOperation(operation) {
3623
5582
  const lines = [
3624
5583
  `${operation.kind}: ${operation.name}`,
@@ -3633,6 +5592,138 @@ function formatPreparedOperation(operation) {
3633
5592
  return lines.join('\n');
3634
5593
  }
3635
5594
 
5595
+ class TransactionTransportError extends Error {
5596
+ constructor(status, body) {
5597
+ super(body.message);
5598
+ this.name = 'TransactionTransportError';
5599
+ this.status = status;
5600
+ this.code = body.code;
5601
+ this.retryable = body.retryable;
5602
+ this.requestId = body.request_id;
5603
+ this.submissionState = body.submission_state;
5604
+ this.signature = body.signature;
5605
+ this.details = body.details;
5606
+ }
5607
+ }
5608
+ function decimal(value) {
5609
+ return value === undefined ? undefined : value.toString(10);
5610
+ }
5611
+ function bigintField(value, field) {
5612
+ if (typeof value !== 'string' || !/^\d+$/.test(value)) {
5613
+ throw new Error(`Invalid decimal u64 field '${field}' in transaction response`);
5614
+ }
5615
+ return BigInt(value);
5616
+ }
5617
+ function optionalBigint(value, field) {
5618
+ return value === undefined || value === null ? undefined : bigintField(value, field);
5619
+ }
5620
+ function requestBody(value) {
5621
+ return JSON.stringify(Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)));
5622
+ }
5623
+ async function parseError(response) {
5624
+ let body = {};
5625
+ try {
5626
+ body = await response.json();
5627
+ }
5628
+ catch {
5629
+ // Public errors are deliberately synthesized without reflecting raw bodies.
5630
+ }
5631
+ return new TransactionTransportError(response.status, {
5632
+ code: typeof body.code === 'string' ? body.code : 'transaction_transport_error',
5633
+ message: typeof body.message === 'string' ? body.message : `Transaction request failed (${response.status})`,
5634
+ retryable: body.retryable === true,
5635
+ request_id: body.request_id ?? body.requestId,
5636
+ submission_state: body.submission_state
5637
+ ?? body.submissionState,
5638
+ signature: body.signature,
5639
+ details: body.details,
5640
+ });
5641
+ }
5642
+ function createTransactionTransport(baseUrl, authenticatedFetch) {
5643
+ const root = `${baseUrl.replace(/\/$/, '')}/transactions/v1`;
5644
+ const post = async (route, body, scope) => {
5645
+ const response = await authenticatedFetch(`${root}/${route}`, {
5646
+ method: 'POST',
5647
+ headers: { 'content-type': 'application/json' },
5648
+ body: requestBody(body),
5649
+ }, scope, true);
5650
+ if (!response.ok)
5651
+ throw await parseError(response);
5652
+ return response.json();
5653
+ };
5654
+ return {
5655
+ async getLatestBlockhash(options = {}) {
5656
+ const value = await post('latest-blockhash', {
5657
+ commitment: options.commitment,
5658
+ minContextSlot: decimal(options.minContextSlot),
5659
+ }, 'transaction:inspect');
5660
+ return {
5661
+ blockhash: String(value.blockhash),
5662
+ contextSlot: bigintField(value.contextSlot, 'contextSlot'),
5663
+ lastValidBlockHeight: bigintField(value.lastValidBlockHeight, 'lastValidBlockHeight'),
5664
+ };
5665
+ },
5666
+ async getFeeForMessage(message, options = {}) {
5667
+ const value = await post('fee', {
5668
+ message,
5669
+ commitment: options.commitment,
5670
+ minContextSlot: decimal(options.minContextSlot),
5671
+ }, 'transaction:inspect');
5672
+ return {
5673
+ feeLamports: value.feeLamports === null ? null : bigintField(value.feeLamports, 'feeLamports'),
5674
+ contextSlot: bigintField(value.contextSlot, 'contextSlot'),
5675
+ };
5676
+ },
5677
+ async simulateTransaction(transaction, options = {}) {
5678
+ const value = await post('simulate', {
5679
+ transaction,
5680
+ commitment: options.commitment,
5681
+ minContextSlot: decimal(options.minContextSlot),
5682
+ accounts: options.accounts ? { addresses: options.accounts } : undefined,
5683
+ innerInstructions: options.innerInstructions,
5684
+ replaceRecentBlockhash: options.replaceRecentBlockhash,
5685
+ }, 'transaction:inspect');
5686
+ return {
5687
+ contextSlot: bigintField(value.contextSlot, 'contextSlot'),
5688
+ err: value.err ?? null,
5689
+ logs: value.logs ?? null,
5690
+ unitsConsumed: optionalBigint(value.unitsConsumed, 'unitsConsumed'),
5691
+ accounts: value.accounts,
5692
+ };
5693
+ },
5694
+ sendTransaction(transaction, options = {}) {
5695
+ return post('send', {
5696
+ transaction,
5697
+ skipPreflight: options.skipPreflight,
5698
+ preflightCommitment: options.preflightCommitment,
5699
+ minContextSlot: decimal(options.minContextSlot),
5700
+ }, 'transaction:send');
5701
+ },
5702
+ async getSignatureStatus(signature, options = {}) {
5703
+ const value = await post('signature-status', {
5704
+ signature,
5705
+ searchTransactionHistory: options.searchTransactionHistory,
5706
+ }, 'transaction:inspect');
5707
+ const status = value?.status;
5708
+ if (!status)
5709
+ return null;
5710
+ return {
5711
+ signature,
5712
+ slot: status.slot === null ? null : bigintField(status.slot, 'slot'),
5713
+ confirmationStatus: status.confirmationStatus,
5714
+ err: status.err ?? null,
5715
+ };
5716
+ },
5717
+ async getBlockHeight(options = {}) {
5718
+ const value = await post('block-height', {
5719
+ commitment: options.commitment,
5720
+ minContextSlot: decimal(options.minContextSlot),
5721
+ }, 'transaction:inspect');
5722
+ return bigintField(value.blockHeight, 'blockHeight');
5723
+ },
5724
+ };
5725
+ }
5726
+
3636
5727
  function mergeAttachedPrograms(stack, attachedPrograms) {
3637
5728
  const merged = { ...(attachedPrograms ?? {}) };
3638
5729
  for (const [name, definition] of Object.entries(stack.programs ?? {})) {
@@ -3678,6 +5769,29 @@ function cloneStackWithPrograms(stack, programs) {
3678
5769
  function withPrograms(stack, attachedPrograms) {
3679
5770
  return cloneStackWithPrograms(stack, mergeAttachedPrograms(stack, attachedPrograms));
3680
5771
  }
5772
+ function composeExecutionCallbacks(defaultCallback, callCallback) {
5773
+ if (!defaultCallback) {
5774
+ return callCallback
5775
+ ? async (value) => { await callCallback(value); }
5776
+ : undefined;
5777
+ }
5778
+ if (!callCallback) {
5779
+ return async (value) => { await defaultCallback(value); };
5780
+ }
5781
+ return async (value) => {
5782
+ const errors = [];
5783
+ for (const callback of [defaultCallback, callCallback]) {
5784
+ try {
5785
+ await callback(value);
5786
+ }
5787
+ catch (error) {
5788
+ errors.push(error);
5789
+ }
5790
+ }
5791
+ if (errors.length > 0)
5792
+ throw errors[0];
5793
+ };
5794
+ }
3681
5795
  let Arete$1 = class Arete {
3682
5796
  constructor(url, httpBaseUrl, options) {
3683
5797
  this.stack = options.stack;
@@ -3686,25 +5800,38 @@ let Arete$1 = class Arete {
3686
5800
  this.httpBaseUrl = httpBaseUrl;
3687
5801
  this.fetchImpl = options.fetch ?? this.resolveFetchImpl();
3688
5802
  this.storage = new SortedStorageDecorator(options.storage ?? new MemoryAdapter());
5803
+ this.queryStore = new QueryStore(this.storage);
3689
5804
  this.processor = new FrameProcessor(this.storage, {
3690
5805
  maxEntriesPerView: options.maxEntriesPerView,
3691
5806
  flushIntervalMs: options.flushIntervalMs,
3692
5807
  schemas: this.stack.schemas,
3693
5808
  patchSchemas: this.stack.patchSchemas,
5809
+ queryStore: this.queryStore,
5810
+ warnOnValidationError: options.validateFrames !== false,
5811
+ onValidationError: options.onFrameValidationError,
3694
5812
  });
3695
5813
  this.connection = new ConnectionManager({
3696
5814
  websocketUrl: url,
5815
+ autoReconnect: options.autoReconnect,
3697
5816
  reconnectIntervals: options.reconnectIntervals,
3698
5817
  maxReconnectAttempts: options.maxReconnectAttempts,
3699
5818
  auth: options.auth,
3700
5819
  });
3701
- this.subscriptionRegistry = new SubscriptionRegistry(this.connection);
5820
+ this.subscriptionRegistry = new SubscriptionRegistry(this.connection, this.queryStore);
3702
5821
  this.connection.onFrame((frame) => {
3703
5822
  this.processor.handleFrame(frame);
3704
5823
  });
5824
+ this.connection.onStateChange((state) => {
5825
+ this.subscriptionRegistry.handleConnectionState(state);
5826
+ });
3705
5827
  this._views = createTypedViews(this.stack, this.storage, this.subscriptionRegistry);
3706
5828
  this._queries = this.buildQueries();
3707
- this._chain = createChainClient(this.httpBaseUrl, this.authenticatedFetch.bind(this));
5829
+ this._chain = createChainClient(this.httpBaseUrl, ((input, init) => this.authenticatedFetch(typeof input === 'string'
5830
+ ? input
5831
+ : input instanceof URL
5832
+ ? input.toString()
5833
+ : input.url, init, ['read'])));
5834
+ this._transactions = createTransactionTransport(this.httpBaseUrl, (input, init, scope, requirePreDispatchMarker) => this.authenticatedFetch(input, init, [scope], requirePreDispatchMarker));
3708
5835
  this._programs = this.buildPrograms();
3709
5836
  }
3710
5837
  resolveFetchImpl() {
@@ -3819,12 +5946,12 @@ let Arete$1 = class Arete {
3819
5946
  method: options?.method ?? 'GET',
3820
5947
  headers: options?.body === undefined ? undefined : { 'content-type': 'application/json' },
3821
5948
  body: options?.body === undefined ? undefined : JSON.stringify(options.body),
3822
- });
5949
+ }, ['read']);
3823
5950
  return parseReadResponse(response, path);
3824
5951
  }
3825
- async authenticatedFetch(input, init) {
5952
+ async authenticatedFetch(input, init, requiredScopes = ['read'], requirePreDispatchMarker = false) {
3826
5953
  const attempt = async (forceRefresh = false) => {
3827
- const token = await this.connection.getHttpAuthToken(forceRefresh);
5954
+ const token = await this.connection.getHttpAuthToken(requiredScopes, forceRefresh);
3828
5955
  const headers = new Headers(init?.headers ?? undefined);
3829
5956
  if (token) {
3830
5957
  headers.set('authorization', `Bearer ${token}`);
@@ -3838,7 +5965,10 @@ let Arete$1 = class Arete {
3838
5965
  if (!response.ok) {
3839
5966
  const wireErrorCode = response.headers.get('X-Error-Code');
3840
5967
  const errorCode = wireErrorCode ? parseErrorCode(wireErrorCode) : undefined;
3841
- if (errorCode && shouldRefreshToken(errorCode)) {
5968
+ const explicitlyNotDispatched = response.headers.get('X-Arete-Upstream-Attempted') === 'false';
5969
+ if (errorCode
5970
+ && shouldRefreshToken(errorCode)
5971
+ && (!requirePreDispatchMarker || explicitlyNotDispatched)) {
3842
5972
  this.connection.clearHttpAuthToken();
3843
5973
  response = await attempt(true);
3844
5974
  }
@@ -3858,8 +5988,8 @@ let Arete$1 = class Arete {
3858
5988
  }
3859
5989
  static async connect(stack, options) {
3860
5990
  const requestedUrl = options?.url ?? stack.endpoints.ws;
3861
- const autoReconnect = options?.autoReconnect !== false;
3862
- const httpOnly = options?.transport === 'http' || (!requestedUrl && !autoReconnect);
5991
+ const autoConnect = options?.autoConnect !== false;
5992
+ const httpOnly = options?.transport === 'http';
3863
5993
  const url = httpOnly ? null : requestedUrl;
3864
5994
  const httpUrl = options?.httpUrl
3865
5995
  ?? stack.endpoints.http
@@ -3878,17 +6008,19 @@ let Arete$1 = class Arete {
3878
6008
  storage: options?.storage,
3879
6009
  maxEntriesPerView: options?.maxEntriesPerView,
3880
6010
  flushIntervalMs: options?.flushIntervalMs,
6011
+ autoConnect: options?.autoConnect,
3881
6012
  autoReconnect: options?.autoReconnect,
3882
6013
  reconnectIntervals: options?.reconnectIntervals,
3883
6014
  maxReconnectAttempts: options?.maxReconnectAttempts,
3884
6015
  validateFrames: options?.validateFrames,
6016
+ onFrameValidationError: options?.onFrameValidationError,
3885
6017
  auth: options?.auth,
3886
6018
  wallet: options?.wallet,
3887
6019
  fetch: options?.fetch,
3888
6020
  execution: options?.execution,
3889
6021
  };
3890
6022
  const client = new Arete(url, httpUrl, internalOptions);
3891
- if (!httpOnly && autoReconnect) {
6023
+ if (!httpOnly && autoConnect) {
3892
6024
  await client.connection.connect();
3893
6025
  }
3894
6026
  return applyConnectedStackExtensions(client, effectiveStack);
@@ -3905,6 +6037,9 @@ let Arete$1 = class Arete {
3905
6037
  get chain() {
3906
6038
  return this._chain;
3907
6039
  }
6040
+ get transactions() {
6041
+ return this._transactions;
6042
+ }
3908
6043
  /** The default wallet adapter, if one was configured. */
3909
6044
  get wallet() {
3910
6045
  return this._wallet;
@@ -3935,7 +6070,12 @@ let Arete$1 = class Arete {
3935
6070
  async transaction(instructions, options) {
3936
6071
  const wallet = options?.wallet ?? this._wallet;
3937
6072
  if (!wallet) {
3938
- throw new Error('Wallet required to sign and send transaction');
6073
+ const cause = new Error('Wallet required to sign and send transaction');
6074
+ throw new TransactionExecutionError({
6075
+ status: 'not-submitted',
6076
+ phase: 'wallet',
6077
+ cause,
6078
+ });
3939
6079
  }
3940
6080
  try {
3941
6081
  const sendOptions = {
@@ -3944,15 +6084,13 @@ let Arete$1 = class Arete {
3944
6084
  if (options?.signers !== undefined) {
3945
6085
  sendOptions.signers = options.signers;
3946
6086
  }
3947
- const result = await wallet.signAndSend(instructions, sendOptions);
6087
+ const result = await wallet.signAndSend(instructions, sendOptions, {
6088
+ transactionTransport: options?.transactionTransport ?? this._transactions,
6089
+ });
3948
6090
  return { signature: result.signature, slot: result.slot };
3949
6091
  }
3950
6092
  catch (err) {
3951
- const programError = parseInstructionError(err, options?.errors ?? this.aggregateErrors());
3952
- if (programError) {
3953
- throw new InstructionError(`${programError.name} (${programError.code}): ${programError.message}`, programError, err);
3954
- }
3955
- throw err;
6093
+ throw normalizeTransactionError(err, options?.errors ?? this.aggregateErrors());
3956
6094
  }
3957
6095
  }
3958
6096
  execute(prepared, options) {
@@ -3963,12 +6101,18 @@ let Arete$1 = class Arete {
3963
6101
  ? { ...(defaults?.send ?? {}), ...(options?.send ?? {}) }
3964
6102
  : undefined,
3965
6103
  signers: options?.signers ?? defaults?.signers,
6104
+ transactionTransport: options?.transactionTransport ?? defaults?.transactionTransport,
3966
6105
  signerRegistry: options?.signerRegistry ?? defaults?.signerRegistry,
3967
6106
  availableSignerAddresses: options?.availableSignerAddresses ?? defaults?.availableSignerAddresses,
3968
- onTransactionStart: options?.onTransactionStart ?? defaults?.onTransactionStart,
3969
- onTransactionSuccess: options?.onTransactionSuccess ?? defaults?.onTransactionSuccess,
6107
+ onTransactionStart: composeExecutionCallbacks(defaults?.onTransactionStart, options?.onTransactionStart),
6108
+ onTransactionSuccess: composeExecutionCallbacks(defaults?.onTransactionSuccess, options?.onTransactionSuccess),
6109
+ onCallbackError: composeExecutionCallbacks(defaults?.onCallbackError, options?.onCallbackError),
3970
6110
  });
3971
6111
  }
6112
+ /** Inspect one prepared instruction/transaction without signing or submitting it. */
6113
+ inspectOperation(prepared, options) {
6114
+ return inspectPreparedOperation(options?.wallet ?? this._wallet, prepared, options?.inspect, { transactionTransport: options?.transactionTransport ?? this._transactions });
6115
+ }
3972
6116
  /** Error metadata from every handler in the stack, deduped by code. */
3973
6117
  aggregateErrors() {
3974
6118
  if (!this._aggregatedErrors) {
@@ -4000,6 +6144,17 @@ let Arete$1 = class Arete {
4000
6144
  get store() {
4001
6145
  return this.storage;
4002
6146
  }
6147
+ /** Highest Solana slot whose streamed frame has been stored locally. */
6148
+ get processedSlot() {
6149
+ return this.processor.getProcessedSlot();
6150
+ }
6151
+ /**
6152
+ * Wait until a frame at or beyond `slot` has been applied to local storage.
6153
+ * Buffered frames satisfy the wait only after their storage batch is flushed.
6154
+ */
6155
+ waitForProcessedSlot(slot, options) {
6156
+ return this.processor.waitForProcessedSlot(slot, options);
6157
+ }
4003
6158
  onConnectionStateChange(callback) {
4004
6159
  return this.connection.onStateChange(callback);
4005
6160
  }
@@ -4031,6 +6186,9 @@ let Arete$1 = class Arete {
4031
6186
  getSubscriptionRegistry() {
4032
6187
  return this.subscriptionRegistry;
4033
6188
  }
6189
+ getQueryStore() {
6190
+ return this.queryStore;
6191
+ }
4034
6192
  };
4035
6193
 
4036
6194
  function createSignerRegistry(entries = []) {
@@ -4084,6 +6242,7 @@ function resolveMemberConnectOptions(stack, member, options, forceHttpOnly) {
4084
6242
  transport,
4085
6243
  auth: member?.auth ?? options?.auth,
4086
6244
  storage: member?.storage,
6245
+ autoConnect: member?.autoConnect,
4087
6246
  autoReconnect: member?.autoReconnect,
4088
6247
  wallet: options?.wallet,
4089
6248
  fetch: options?.fetch,
@@ -4123,6 +6282,7 @@ async function createSession(definition, options) {
4123
6282
  ...connectedPrograms.map(([, client]) => client),
4124
6283
  ];
4125
6284
  const executionHost = memberClients[0];
6285
+ const transactions = options?.transactions ?? executionHost.transactions;
4126
6286
  const stacks = Object.fromEntries(connectedStacks);
4127
6287
  const explicitProgramKeys = new Set(connectedPrograms.map(([key]) => key));
4128
6288
  const programOwners = new Map();
@@ -4160,12 +6320,14 @@ async function createSession(definition, options) {
4160
6320
  },
4161
6321
  signerRegistry,
4162
6322
  chain,
6323
+ transactions,
4163
6324
  transaction(instructions, transactionOptions) {
4164
6325
  const defaults = options?.execution;
4165
6326
  const configuredSigners = transactionOptions?.signers ?? defaults?.signers;
4166
6327
  const signers = [...new Set([...signerRegistry.values(), ...(configuredSigners ?? [])])];
4167
6328
  return executionHost.transaction(instructions, {
4168
6329
  wallet: transactionOptions?.wallet ?? defaults?.wallet,
6330
+ transactionTransport: transactionOptions?.transactionTransport ?? transactions,
4169
6331
  send: defaults?.send || transactionOptions?.send
4170
6332
  ? { ...(defaults?.send ?? {}), ...(transactionOptions?.send ?? {}) }
4171
6333
  : undefined,
@@ -4178,6 +6340,7 @@ async function createSession(definition, options) {
4178
6340
  const configuredSigners = executionOptions?.signers ?? defaults?.signers;
4179
6341
  return executionHost.execute(prepared, {
4180
6342
  wallet: executionOptions?.wallet ?? defaults?.wallet,
6343
+ transactionTransport: executionOptions?.transactionTransport ?? defaults?.transactionTransport ?? transactions,
4181
6344
  send: defaults?.send || executionOptions?.send
4182
6345
  ? { ...(defaults?.send ?? {}), ...(executionOptions?.send ?? {}) }
4183
6346
  : undefined,
@@ -4186,6 +6349,7 @@ async function createSession(definition, options) {
4186
6349
  availableSignerAddresses: executionOptions?.availableSignerAddresses ?? defaults?.availableSignerAddresses,
4187
6350
  onTransactionStart: executionOptions?.onTransactionStart ?? defaults?.onTransactionStart,
4188
6351
  onTransactionSuccess: executionOptions?.onTransactionSuccess ?? defaults?.onTransactionSuccess,
6352
+ onCallbackError: executionOptions?.onCallbackError ?? defaults?.onCallbackError,
4189
6353
  });
4190
6354
  },
4191
6355
  setWallet(nextWallet) {
@@ -4301,6 +6465,15 @@ function toRawAmount(amount, decimals) {
4301
6465
  }
4302
6466
  return parseUiAmountToRaw(amount.ui, decimals);
4303
6467
  }
6468
+ /** Convert an amount to raw units without throwing on invalid user input. */
6469
+ function safeToRawAmount(amount, decimals) {
6470
+ try {
6471
+ return { success: true, data: toRawAmount(amount, decimals) };
6472
+ }
6473
+ catch (error) {
6474
+ return { success: false, error };
6475
+ }
6476
+ }
4304
6477
  /** Fetch a mint's decimals via the chain read endpoint, throwing when unavailable. */
4305
6478
  async function getMintDecimals(chain, mint) {
4306
6479
  const account = await chain.mint(mint);
@@ -4349,7 +6522,28 @@ async function resolveAmountsToRaw(chain, inputs) {
4349
6522
  return Object.fromEntries(entries);
4350
6523
  }
4351
6524
 
6525
+ function isPlainObject(value) {
6526
+ const prototype = Object.getPrototypeOf(value);
6527
+ return prototype === Object.prototype || prototype === null;
6528
+ }
6529
+ function stringifyBigints(value) {
6530
+ if (typeof value === 'bigint') {
6531
+ return String(value);
6532
+ }
6533
+ if (Array.isArray(value)) {
6534
+ return value.map((entry) => stringifyBigints(entry));
6535
+ }
6536
+ if (value !== null && typeof value === 'object' && isPlainObject(value)) {
6537
+ const out = {};
6538
+ for (const [key, entry] of Object.entries(value)) {
6539
+ out[key] = stringifyBigints(entry);
6540
+ }
6541
+ return out;
6542
+ }
6543
+ return value;
6544
+ }
6545
+
4352
6546
  const Arete = Object.assign(Arete$1, { session: createSession });
4353
6547
 
4354
- export { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, Arete, AreteError, ConnectionManager, DEFAULT_CONFIG, DEFAULT_MAX_ENTRIES_PER_VIEW, FrameProcessor, InstructionError, MemoryAdapter, OperationExecutionError, PROGRAM_OPERATION_EXTENSIONS, ReadRequestError, SPL_TOKEN_PROGRAM_ADDRESS, STACK_RUNTIME_EXTENSIONS, SYSTEM_PROGRAM_ADDRESS, SubscriptionRegistry, TOKEN_2022_PROGRAM_ADDRESS, account, appendFlowTransactions, appendTransactionInstructions, applyConnectedStackExtensions, arg, buildInstruction, bytes, chainAccountLoader, createChainClient, createEntityStream, createInstructionExecutor, createInstructionHandler, createPreparedFlow, createPreparedInstruction, createPreparedTransaction, createPreparedTransactionBody, createProgramPdas, createPublicKeySeed, createRichUpdateStream, createSeed, createSession, createSignerRegistry, createTypedListView, createTypedStateView, createTypedViews, createUpdateStream, decodeBase58, defineProgramExtensions, defineStackExtensions, deriveAssociatedTokenAccount, deriveHttpEndpoint, findProgramAddress as derivePda, describePreparedOperation, encodeBase58, executeInstruction, executePreparedOperation, extendProgram, extendPrograms, extendStack, findProgramAddress, findProgramAddressSync, flowOperation, formatPreparedOperation, formatProgramError, formatRawToUi, getMintDecimals, getProgramRuntimeExtensions, getStackRuntimeExtensions, instructionOperation, isEntityFrame, isSnapshotFrame, isSubscribedFrame, isValidFrame, literal, parseFrame, parseFrameFromBlob, parseInstructionError, parseUiAmountToRaw, pda, prependFlowTransactionInstructions, prependTransactionInstructions, programAccountRead, programQuery, resolveAccounts, resolveAmount, resolveAmountToRaw, resolveAmountsToRaw, resolveTokenProgramAddress, serializeInstructionData, stackQuery, toJsonValue, toRawAmount, transactionOperation, validateAccountResolution, withPrograms };
6548
+ export { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, Arete, AreteError, ConnectionManager, DEFAULT_CONFIG, DEFAULT_MAX_ENTRIES_PER_VIEW, FrameProcessor, InstructionError, MemoryAdapter, OperationCallbackError, OperationExecutionError, PROGRAM_OPERATION_EXTENSIONS, ProcessedSlotTimeoutError, QueryStore, ReadRequestError, SPL_TOKEN_PROGRAM_ADDRESS, STACK_RUNTIME_EXTENSIONS, SYSTEM_PROGRAM_ADDRESS, SubscriptionRegistry, TOKEN_2022_PROGRAM_ADDRESS, TransactionExecutionError, TransactionTransportError, account, appendFlowTransactions, appendTransactionInstructions, applyConnectedStackExtensions, arg, buildInstruction, bytes, canonicalQueryKey, chainAccountLoader, createChainClient, createEntityStream, createInstructionExecutor, createInstructionHandler, createPreparedFlow, createPreparedInstruction, createPreparedTransaction, createPreparedTransactionBody, createProgramPdas, createPublicKeySeed, createRichUpdateStream, createSeed, createSession, createSignerRegistry, createSubscriptionId, createTransactionTransport, createTypedListView, createTypedStateView, createTypedViews, createUpdateStream, decodeBase58, defineProgramExtensions, defineStackExtensions, deriveAssociatedTokenAccount, deriveHttpEndpoint, findProgramAddress as derivePda, describePreparedOperation, encodeBase58, executeInstruction, executePreparedOperation, extendProgram, extendPrograms, extendStack, findProgramAddress, findProgramAddressSync, flowOperation, formatPreparedOperation, formatProgramError, formatRawToUi, getMintDecimals, getProgramRuntimeExtensions, getStackRuntimeExtensions, getTransactionFailureOutcome, inspectPreparedOperation, instructionOperation, isEntityFrame, isErrorFrame, isHostedAreteEndpoint, isSnapshotFrame, isSubscribedFrame, isUnsubscribedFrame, isValidFrame, literal, normalizeSubscription, normalizeSubscriptionQuery, normalizeSubscriptionRequest, normalizeTransactionError, parseFrame, parseFrameFromBlob, parseInstructionError, parseUiAmountToRaw, pda, prependFlowTransactionInstructions, prependTransactionInstructions, programAccountRead, programQuery, resolveAccounts, resolveAmount, resolveAmountToRaw, resolveAmountsToRaw, resolveTokenProgramAddress, safeToRawAmount, serializeInstructionData, serializeViewKey, stackQuery, stringifyBigints, toJsonValue, toRawAmount, transactionOperation, unwrapOperationExecutionError, validateAccountResolution, validateSubscriptionId, withPrograms };
4355
6549
  //# sourceMappingURL=index.esm.js.map