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