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