@usearete/sdk 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/dist/{index.js → index.cjs} +2725 -509
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +597 -118
- package/dist/index.esm.js +2666 -472
- package/dist/index.esm.js.map +1 -1
- package/dist/ssr/handlers.cjs +794 -0
- package/dist/ssr/handlers.cjs.map +1 -0
- package/dist/ssr/handlers.esm.js +523 -4
- package/dist/ssr/handlers.esm.js.map +1 -1
- package/dist/ssr/index.cjs +795 -0
- package/dist/ssr/index.cjs.map +1 -0
- package/dist/ssr/index.esm.js +523 -4
- package/dist/ssr/index.esm.js.map +1 -1
- package/dist/ssr/nextjs-app.cjs +859 -0
- package/dist/ssr/nextjs-app.cjs.map +1 -0
- package/dist/ssr/nextjs-app.esm.js +523 -4
- package/dist/ssr/nextjs-app.esm.js.map +1 -1
- package/dist/ssr/tanstack-start.cjs +881 -0
- package/dist/ssr/tanstack-start.cjs.map +1 -0
- package/dist/ssr/tanstack-start.esm.js +523 -4
- package/dist/ssr/tanstack-start.esm.js.map +1 -1
- package/dist/ssr/vite.cjs +887 -0
- package/dist/ssr/vite.cjs.map +1 -0
- package/dist/ssr/vite.esm.js +523 -4
- package/dist/ssr/vite.esm.js.map +1 -1
- package/package.json +10 -8
- package/dist/index.js.map +0 -1
- package/dist/ssr/handlers.js +0 -294
- package/dist/ssr/handlers.js.map +0 -1
- package/dist/ssr/index.js +0 -295
- package/dist/ssr/index.js.map +0 -1
- package/dist/ssr/nextjs-app.js +0 -359
- package/dist/ssr/nextjs-app.js.map +0 -1
- package/dist/ssr/tanstack-start.js +0 -381
- package/dist/ssr/tanstack-start.js.map +0 -1
- package/dist/ssr/vite.js +0 -387
- package/dist/ssr/vite.js.map +0 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var pako = require('pako');
|
|
4
|
-
var
|
|
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
|
|
147
|
+
return 'op' in frame && LIVE_OPS.has(frame.op);
|
|
83
148
|
}
|
|
84
|
-
function
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
return JSON.parse(jsonString);
|
|
161
|
+
return frame;
|
|
96
162
|
}
|
|
97
163
|
async function parseFrameFromBlob(blob) {
|
|
98
|
-
|
|
99
|
-
return parseFrame(arrayBuffer);
|
|
164
|
+
return parseFrame(await blob.arrayBuffer());
|
|
100
165
|
}
|
|
101
166
|
function isValidFrame(frame) {
|
|
102
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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['
|
|
181
|
-
&& typeof candidate['
|
|
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
|
|
575
|
+
function isHostedAreteEndpoint(url) {
|
|
184
576
|
try {
|
|
185
|
-
return new URL(
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
718
|
+
case 'static-token': {
|
|
719
|
+
requireCurrent();
|
|
720
|
+
return this.updateTokenState(strategy.token, scopes);
|
|
721
|
+
}
|
|
281
722
|
case 'token-provider':
|
|
282
723
|
try {
|
|
283
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
849
|
+
if (this.tokenRefreshInFlight === refresh) {
|
|
850
|
+
this.scheduleTokenRefresh();
|
|
851
|
+
}
|
|
402
852
|
}
|
|
403
853
|
catch {
|
|
404
|
-
this.
|
|
854
|
+
if (this.tokenRefreshInFlight === refresh) {
|
|
855
|
+
this.scheduleTokenRefresh();
|
|
856
|
+
}
|
|
405
857
|
}
|
|
406
858
|
finally {
|
|
407
|
-
this.tokenRefreshInFlight
|
|
859
|
+
if (this.tokenRefreshInFlight === refresh) {
|
|
860
|
+
this.tokenRefreshInFlight = null;
|
|
861
|
+
}
|
|
408
862
|
}
|
|
409
863
|
})();
|
|
410
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
1019
|
+
token = await Promise.race([
|
|
1020
|
+
this.getOrRefreshToken(false, ['read']),
|
|
1021
|
+
cancellation,
|
|
1022
|
+
]);
|
|
546
1023
|
}
|
|
547
1024
|
catch (error) {
|
|
548
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
563
|
-
this.ws
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
685
|
-
|
|
1228
|
+
try {
|
|
1229
|
+
this.ws.send(JSON.stringify(normalized));
|
|
686
1230
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
693
|
-
if (!alreadyQueued) {
|
|
694
|
-
this.subscriptionQueue.push(subscription);
|
|
695
|
-
}
|
|
1248
|
+
this.subscriptionQueue.set(subscriptionId, normalized);
|
|
696
1249
|
}
|
|
697
1250
|
}
|
|
698
|
-
unsubscribe(
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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',
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
873
|
-
|
|
874
|
-
|
|
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
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
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
|
-
|
|
941
|
-
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
-
|
|
1005
|
-
this.
|
|
1006
|
-
}
|
|
1007
|
-
this.isProcessing = false;
|
|
1683
|
+
finally {
|
|
1684
|
+
this.isProcessing = false;
|
|
1685
|
+
}
|
|
1008
1686
|
}
|
|
1009
1687
|
processFrame(frame) {
|
|
1010
|
-
if (
|
|
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 (
|
|
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(
|
|
1037
|
-
sort: this.normalizeSortConfig(
|
|
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
|
-
|
|
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
|
-
|
|
1863
|
+
const update = {
|
|
1081
1864
|
type: 'upsert',
|
|
1082
1865
|
key: frame.key,
|
|
1083
1866
|
data: nextValue,
|
|
1084
|
-
}
|
|
1085
|
-
this.
|
|
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,
|
|
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
|
-
|
|
1901
|
+
const update = {
|
|
1101
1902
|
type: 'patch',
|
|
1102
1903
|
key: frame.key,
|
|
1103
1904
|
data: normalizedPatch,
|
|
1104
|
-
}
|
|
1105
|
-
this.
|
|
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
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
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
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
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
|
-
|
|
1493
|
-
this.
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
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
|
-
|
|
2431
|
+
this.touch(record);
|
|
1500
2432
|
}
|
|
1501
|
-
|
|
1502
|
-
const
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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
|
-
|
|
1513
|
-
const
|
|
1514
|
-
|
|
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
|
-
|
|
1517
|
-
|
|
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
|
|
1521
|
-
this.
|
|
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.
|
|
2579
|
+
this.records.clear();
|
|
1524
2580
|
}
|
|
1525
|
-
|
|
1526
|
-
const
|
|
1527
|
-
|
|
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
|
|
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
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
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(
|
|
2732
|
+
function createEntityStream(_storage, subscriptionRegistry, query, options, keyFilter) {
|
|
1597
2733
|
const schema = options?.schema;
|
|
1598
2734
|
return {
|
|
1599
2735
|
[Symbol.asyncIterator]() {
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
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
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
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(
|
|
2768
|
+
function createRichUpdateStream(_storage, subscriptionRegistry, query, keyFilter, snapshotEnabled = true) {
|
|
1673
2769
|
return {
|
|
1674
2770
|
[Symbol.asyncIterator]() {
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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
|
|
1741
|
-
|
|
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
|
|
1745
|
-
|
|
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
|
|
1749
|
-
|
|
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
|
|
2839
|
+
async get(key, options) {
|
|
2840
|
+
return this.getSync(key, options) ?? null;
|
|
1753
2841
|
},
|
|
1754
|
-
getSync(key) {
|
|
1755
|
-
|
|
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 {
|
|
1763
|
-
return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, ...
|
|
2856
|
+
const { query } = queryOptions(options);
|
|
2857
|
+
return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, ...query }, options);
|
|
1764
2858
|
},
|
|
1765
2859
|
watch(options) {
|
|
1766
|
-
const {
|
|
1767
|
-
return createUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...
|
|
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 {
|
|
1771
|
-
return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...
|
|
2864
|
+
const { query, snapshotEnabled } = queryOptions(options);
|
|
2865
|
+
return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...query }, undefined, snapshotEnabled);
|
|
1772
2866
|
},
|
|
1773
|
-
async get() {
|
|
1774
|
-
return
|
|
2867
|
+
async get(options) {
|
|
2868
|
+
return this.getSync(options) ?? [];
|
|
1775
2869
|
},
|
|
1776
|
-
getSync() {
|
|
1777
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2913
|
-
|
|
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
|
-
|
|
2916
|
-
|
|
2917
|
-
return null;
|
|
4522
|
+
if (seen.has(error)) {
|
|
4523
|
+
return;
|
|
2918
4524
|
}
|
|
2919
|
-
|
|
2920
|
-
if (
|
|
2921
|
-
|
|
2922
|
-
|
|
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
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
}
|
|
2933
|
-
|
|
2934
|
-
|
|
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
|
-
|
|
2942
|
-
|
|
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
|
-
|
|
4559
|
+
results.push({ code: errorObj.code, source: 'direct-code' });
|
|
2948
4560
|
}
|
|
2949
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3495
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
3864
|
-
const httpOnly = options?.transport === 'http'
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
3971
|
-
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.
|
|
6664
|
+
//# sourceMappingURL=index.cjs.map
|