@usearete/sdk 0.24.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -1
- package/dist/index.cjs +701 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +270 -26
- package/dist/index.esm.js +695 -116
- package/dist/index.esm.js.map +1 -1
- package/dist/testing.cjs +619 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.ts +482 -0
- package/dist/testing.esm.js +603 -0
- package/dist/testing.esm.js.map +1 -0
- package/package.json +18 -2
package/dist/index.cjs
CHANGED
|
@@ -444,6 +444,7 @@ class SubscriptionRegistry {
|
|
|
444
444
|
this.queryStore = queryStore;
|
|
445
445
|
this.subscriptions = new Map();
|
|
446
446
|
this.subscriptionsById = new Map();
|
|
447
|
+
this.failureListeners = new Set();
|
|
447
448
|
}
|
|
448
449
|
subscribe(query, snapshotEnabled = true) {
|
|
449
450
|
const request = normalizeSubscriptionRequest({
|
|
@@ -506,12 +507,30 @@ class SubscriptionRegistry {
|
|
|
506
507
|
? this.queryStore.getSnapshot(tracker.subscription.subscriptionId)
|
|
507
508
|
: undefined;
|
|
508
509
|
}
|
|
509
|
-
handleConnectionState(state) {
|
|
510
|
+
handleConnectionState(state, message) {
|
|
510
511
|
if (state === 'reconnecting')
|
|
511
512
|
this.queryStore.beginReconnect();
|
|
512
|
-
if (state
|
|
513
|
-
this.
|
|
513
|
+
if (state !== 'error') {
|
|
514
|
+
this.connectionError = undefined;
|
|
515
|
+
return;
|
|
514
516
|
}
|
|
517
|
+
this.queryStore.failRefreshing(new AreteError('Connection failed while refreshing subscriptions', 'CONNECTION_ERROR'));
|
|
518
|
+
// Queries still waiting for their first snapshot are not failed above;
|
|
519
|
+
// one-shot reads learn about the failure here instead of timing out.
|
|
520
|
+
this.connectionError = new AreteError(message ?? 'Connection failed', 'CONNECTION_ERROR');
|
|
521
|
+
this.notifyFailure(this.connectionError);
|
|
522
|
+
}
|
|
523
|
+
/** The terminal connection failure while the connection is in the `error` state. */
|
|
524
|
+
getConnectionError() {
|
|
525
|
+
return this.connectionError;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Called when pending queries can no longer resolve: the connection failed
|
|
529
|
+
* terminally, or a disconnect cleared every subscription.
|
|
530
|
+
*/
|
|
531
|
+
onFailure(callback) {
|
|
532
|
+
this.failureListeners.add(callback);
|
|
533
|
+
return () => this.failureListeners.delete(callback);
|
|
515
534
|
}
|
|
516
535
|
clear() {
|
|
517
536
|
for (const { subscription } of this.subscriptions.values()) {
|
|
@@ -527,6 +546,11 @@ class SubscriptionRegistry {
|
|
|
527
546
|
}
|
|
528
547
|
this.subscriptions.clear();
|
|
529
548
|
this.subscriptionsById.clear();
|
|
549
|
+
this.notifyFailure(new AreteError('Subscriptions were cleared by a disconnect', 'CONNECTION_CANCELLED'));
|
|
550
|
+
}
|
|
551
|
+
notifyFailure(error) {
|
|
552
|
+
for (const listener of [...this.failureListeners])
|
|
553
|
+
listener(error);
|
|
530
554
|
}
|
|
531
555
|
createLease(tracker) {
|
|
532
556
|
let released = false;
|
|
@@ -1874,6 +1898,7 @@ class ConnectionManager {
|
|
|
1874
1898
|
}
|
|
1875
1899
|
|
|
1876
1900
|
const INTERNAL_SEQ_FIELD = '__seq';
|
|
1901
|
+
const INTERNAL_VERSION_FIELD = '__version';
|
|
1877
1902
|
class ProcessedSlotTimeoutError extends Error {
|
|
1878
1903
|
constructor(targetSlot, processedSlot) {
|
|
1879
1904
|
super(`Timed out waiting for Arete to process slot ${targetSlot}`);
|
|
@@ -1934,6 +1959,9 @@ function toCamelCaseSegment(value) {
|
|
|
1934
1959
|
if (value === '_seq') {
|
|
1935
1960
|
return INTERNAL_SEQ_FIELD;
|
|
1936
1961
|
}
|
|
1962
|
+
if (value === '_version') {
|
|
1963
|
+
return INTERNAL_VERSION_FIELD;
|
|
1964
|
+
}
|
|
1937
1965
|
const pascal = value
|
|
1938
1966
|
.split(/[_.-]/)
|
|
1939
1967
|
.filter(segment => segment.length > 0)
|
|
@@ -1944,6 +1972,16 @@ function toCamelCaseSegment(value) {
|
|
|
1944
1972
|
}
|
|
1945
1973
|
return pascal[0].toLowerCase() + pascal.slice(1);
|
|
1946
1974
|
}
|
|
1975
|
+
/** `{epoch}:{counter}` from a frame's `_version`, or null if it does not parse. */
|
|
1976
|
+
function parseVersion(version) {
|
|
1977
|
+
const separator = version.lastIndexOf(':');
|
|
1978
|
+
if (separator <= 0)
|
|
1979
|
+
return null;
|
|
1980
|
+
const counter = version.slice(separator + 1);
|
|
1981
|
+
if (!/^\d+$/.test(counter))
|
|
1982
|
+
return null;
|
|
1983
|
+
return { epoch: version.slice(0, separator), counter: BigInt(counter) };
|
|
1984
|
+
}
|
|
1947
1985
|
class FrameProcessor {
|
|
1948
1986
|
/**
|
|
1949
1987
|
* Observe the connection's cursor tracker instead of a private one.
|
|
@@ -1962,6 +2000,15 @@ class FrameProcessor {
|
|
|
1962
2000
|
this.latestProcessedSlot = null;
|
|
1963
2001
|
this.processedSlotWaiters = new Set();
|
|
1964
2002
|
this.cursors = new CursorTracker();
|
|
2003
|
+
/**
|
|
2004
|
+
* Whether the server's latest acknowledgement guaranteed whole entities.
|
|
2005
|
+
* One connection talks to one server, so its latest ack speaks for every
|
|
2006
|
+
* subscription on it; a reconnect re-acknowledges each one.
|
|
2007
|
+
*/
|
|
2008
|
+
this.serverSendsWholeEntities = false;
|
|
2009
|
+
/** Keys dropped by `maxEntriesPerView`, per view, oldest first; bounded. */
|
|
2010
|
+
this.evictedKeys = new Map();
|
|
2011
|
+
this.warnedEvictedViews = new Set();
|
|
1965
2012
|
this.storage = storage;
|
|
1966
2013
|
this.maxEntriesPerView = config.maxEntriesPerView === undefined
|
|
1967
2014
|
? DEFAULT_MAX_ENTRIES_PER_VIEW
|
|
@@ -2009,18 +2056,22 @@ class FrameProcessor {
|
|
|
2009
2056
|
const diagnostic = {
|
|
2010
2057
|
view: viewPath,
|
|
2011
2058
|
...context,
|
|
2059
|
+
reason: 'schema',
|
|
2012
2060
|
error: validationError,
|
|
2013
2061
|
};
|
|
2062
|
+
this.reportDiagnostic(diagnostic);
|
|
2063
|
+
if (this.warnOnValidationError) {
|
|
2064
|
+
console.warn('[Arete] Frame validation failed:', diagnostic);
|
|
2065
|
+
}
|
|
2066
|
+
return null;
|
|
2067
|
+
}
|
|
2068
|
+
reportDiagnostic(diagnostic) {
|
|
2014
2069
|
try {
|
|
2015
2070
|
this.onValidationError?.(diagnostic);
|
|
2016
2071
|
}
|
|
2017
2072
|
catch (callbackError) {
|
|
2018
2073
|
console.error('[Arete] Frame validation callback failed:', callbackError);
|
|
2019
2074
|
}
|
|
2020
|
-
if (this.warnOnValidationError) {
|
|
2021
|
-
console.warn('[Arete] Frame validation failed:', diagnostic);
|
|
2022
|
-
}
|
|
2023
|
-
return null;
|
|
2024
2075
|
}
|
|
2025
2076
|
hasSchema(viewPath) {
|
|
2026
2077
|
return this.getSchema(viewPath) !== null;
|
|
@@ -2087,6 +2138,50 @@ class FrameProcessor {
|
|
|
2087
2138
|
}
|
|
2088
2139
|
return leftIndex.localeCompare(rightIndex);
|
|
2089
2140
|
}
|
|
2141
|
+
extractVersion(data) {
|
|
2142
|
+
if (!isObject(data)) {
|
|
2143
|
+
return undefined;
|
|
2144
|
+
}
|
|
2145
|
+
const version = data._version;
|
|
2146
|
+
return typeof version === 'string' ? version : undefined;
|
|
2147
|
+
}
|
|
2148
|
+
getInternalVersion(data) {
|
|
2149
|
+
if (!isObject(data)) {
|
|
2150
|
+
return undefined;
|
|
2151
|
+
}
|
|
2152
|
+
const version = data[INTERNAL_VERSION_FIELD];
|
|
2153
|
+
return typeof version === 'string' ? version : undefined;
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Whether a frame at `incoming` is one this client already applied, or older.
|
|
2157
|
+
* A version is `{epoch}:{counter}`, and counters compare only within an
|
|
2158
|
+
* epoch: a server restart or a reloaded stack starts a new one, whose first
|
|
2159
|
+
* frame is newer than anything from the old one. A version that does not
|
|
2160
|
+
* parse cannot be ordered and is never stale.
|
|
2161
|
+
*/
|
|
2162
|
+
isStaleVersion(incoming, held) {
|
|
2163
|
+
if (held === undefined) {
|
|
2164
|
+
return false;
|
|
2165
|
+
}
|
|
2166
|
+
const next = parseVersion(incoming);
|
|
2167
|
+
const current = parseVersion(held);
|
|
2168
|
+
if (next === null || current === null || next.epoch !== current.epoch) {
|
|
2169
|
+
return false;
|
|
2170
|
+
}
|
|
2171
|
+
return next.counter <= current.counter;
|
|
2172
|
+
}
|
|
2173
|
+
attachInternalVersion(data, version) {
|
|
2174
|
+
if (!version || !isObject(data)) {
|
|
2175
|
+
return data;
|
|
2176
|
+
}
|
|
2177
|
+
Object.defineProperty(data, INTERNAL_VERSION_FIELD, {
|
|
2178
|
+
value: version,
|
|
2179
|
+
enumerable: false,
|
|
2180
|
+
configurable: true,
|
|
2181
|
+
writable: true,
|
|
2182
|
+
});
|
|
2183
|
+
return data;
|
|
2184
|
+
}
|
|
2090
2185
|
attachInternalSeq(_viewPath, data, seq) {
|
|
2091
2186
|
if (!seq || !isObject(data)) {
|
|
2092
2187
|
return data;
|
|
@@ -2307,6 +2402,7 @@ class FrameProcessor {
|
|
|
2307
2402
|
}
|
|
2308
2403
|
handleSubscribedFrame(frame) {
|
|
2309
2404
|
this.cursors.observe(frame);
|
|
2405
|
+
this.serverSendsWholeEntities = frame.wholeEntities === true;
|
|
2310
2406
|
const viewPath = frame.query.view;
|
|
2311
2407
|
if (this.storage.setViewConfig && frame.sort) {
|
|
2312
2408
|
this.storage.setViewConfig(viewPath, {
|
|
@@ -2331,10 +2427,11 @@ class FrameProcessor {
|
|
|
2331
2427
|
if (normalized === null) {
|
|
2332
2428
|
continue;
|
|
2333
2429
|
}
|
|
2334
|
-
const nextValue = this.attachInternalSeq(viewPath, normalized, this.extractSeq(entity.data));
|
|
2430
|
+
const nextValue = this.attachInternalVersion(this.attachInternalSeq(viewPath, normalized, this.extractSeq(entity.data)), this.extractVersion(entity.data));
|
|
2335
2431
|
acceptedKeys.push(entity.key);
|
|
2336
2432
|
const previousValue = this.storage.get(viewPath, entity.key);
|
|
2337
2433
|
this.storage.set(viewPath, entity.key, nextValue);
|
|
2434
|
+
this.forgetEvicted(viewPath, entity.key);
|
|
2338
2435
|
this.storage.notifyUpdate(viewPath, entity.key, {
|
|
2339
2436
|
type: 'upsert',
|
|
2340
2437
|
key: entity.key,
|
|
@@ -2353,18 +2450,24 @@ class FrameProcessor {
|
|
|
2353
2450
|
const previousValue = this.storage.get(viewPath, frame.key);
|
|
2354
2451
|
const previousSequence = this.getInternalSeq(previousValue);
|
|
2355
2452
|
const cursor = this.cursors.observe(frame);
|
|
2356
|
-
//
|
|
2357
|
-
//
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2453
|
+
// A server that stamps `_version` orders one key's frames by it. `seq`
|
|
2454
|
+
// cannot: every update decoded from one transaction shares one, and within
|
|
2455
|
+
// a slot account updates and instructions number themselves differently,
|
|
2456
|
+
// so a later frame can carry a lower `seq`. Without a version (an older
|
|
2457
|
+
// server) the `seq` rule applies. On a tape the offset is the identity, so
|
|
2458
|
+
// neither does.
|
|
2459
|
+
const frameVersion = this.extractVersion(frame.data);
|
|
2460
|
+
const duplicateOrStale = frame.offset === undefined && (frameVersion !== undefined
|
|
2461
|
+
? this.isStaleVersion(frameVersion, this.getInternalVersion(previousValue))
|
|
2462
|
+
: frame.seq !== undefined
|
|
2463
|
+
&& previousSequence !== undefined
|
|
2464
|
+
&& this.compareSeq(frame.seq, previousSequence) <= 0);
|
|
2362
2465
|
switch (frame.op) {
|
|
2363
2466
|
case 'upsert':
|
|
2364
2467
|
{
|
|
2365
2468
|
if (frame.data === null)
|
|
2366
2469
|
break;
|
|
2367
|
-
if (
|
|
2470
|
+
if (duplicateOrStale && previousValue !== null) {
|
|
2368
2471
|
const update = {
|
|
2369
2472
|
type: 'upsert',
|
|
2370
2473
|
key: frame.key,
|
|
@@ -2381,14 +2484,17 @@ class FrameProcessor {
|
|
|
2381
2484
|
if (normalized === null) {
|
|
2382
2485
|
break;
|
|
2383
2486
|
}
|
|
2384
|
-
const nextValue = this.attachInternalSeq(viewPath, normalized,
|
|
2487
|
+
const nextValue = this.attachInternalVersion(this.attachInternalSeq(viewPath, normalized,
|
|
2385
2488
|
// Fall back to the previous entity's sequence, as the patch branch
|
|
2386
2489
|
// below does. Without this an unsequenced upsert drops the tracked
|
|
2387
2490
|
// sequence (it rides on the object being replaced), disarming the
|
|
2388
2491
|
// staleness guard until the next sequenced frame and letting a
|
|
2389
2492
|
// later older frame overwrite newer data.
|
|
2390
|
-
frame.seq ?? this.extractSeq(frame.data) ?? this.getInternalSeq(previousValue))
|
|
2493
|
+
frame.seq ?? this.extractSeq(frame.data) ?? this.getInternalSeq(previousValue)),
|
|
2494
|
+
// An unversioned write keeps the version it replaces, likewise.
|
|
2495
|
+
frameVersion ?? this.getInternalVersion(previousValue));
|
|
2391
2496
|
this.storage.set(viewPath, frame.key, nextValue);
|
|
2497
|
+
this.forgetEvicted(viewPath, frame.key);
|
|
2392
2498
|
const update = {
|
|
2393
2499
|
type: 'upsert',
|
|
2394
2500
|
key: frame.key,
|
|
@@ -2405,6 +2511,19 @@ class FrameProcessor {
|
|
|
2405
2511
|
if (frame.data === null)
|
|
2406
2512
|
break;
|
|
2407
2513
|
const existing = this.storage.get(viewPath, frame.key);
|
|
2514
|
+
if (existing === null && frame.offset === undefined && this.serverSendsWholeEntities) {
|
|
2515
|
+
// A patch carries only changed fields. With no entity to merge into
|
|
2516
|
+
// (never received, or evicted) it would be stored as a partial
|
|
2517
|
+
// entity typed as complete, so it is discarded: no storage write, no
|
|
2518
|
+
// sequence, no query membership, no update. The key appears when the
|
|
2519
|
+
// server sends a full upsert, which a server acknowledging
|
|
2520
|
+
// `wholeEntities` guarantees for every key it has not sent. Tape
|
|
2521
|
+
// records (frames with an `offset`) are events rather than entity
|
|
2522
|
+
// state and are always applied: a consumer resuming from a cursor
|
|
2523
|
+
// holds what came before it.
|
|
2524
|
+
this.reportDiscardedPatch(viewPath, frame);
|
|
2525
|
+
break;
|
|
2526
|
+
}
|
|
2408
2527
|
const normalizedPatch = this.normalizeEntity(viewPath, frame.data, {
|
|
2409
2528
|
key: frame.key,
|
|
2410
2529
|
operation: 'patch',
|
|
@@ -2413,7 +2532,7 @@ class FrameProcessor {
|
|
|
2413
2532
|
if (normalizedPatch === null) {
|
|
2414
2533
|
break;
|
|
2415
2534
|
}
|
|
2416
|
-
if (
|
|
2535
|
+
if (duplicateOrStale && existing !== null) {
|
|
2417
2536
|
const update = {
|
|
2418
2537
|
type: 'patch',
|
|
2419
2538
|
key: frame.key,
|
|
@@ -2426,7 +2545,7 @@ class FrameProcessor {
|
|
|
2426
2545
|
const merged = existing
|
|
2427
2546
|
? deepMergeWithAppend(existing, normalizedPatch, appendPaths)
|
|
2428
2547
|
: normalizedPatch;
|
|
2429
|
-
const nextValue = this.attachInternalSeq(viewPath, merged, frame.seq ?? this.extractSeq(frame.data) ?? this.getInternalSeq(existing));
|
|
2548
|
+
const nextValue = this.attachInternalVersion(this.attachInternalSeq(viewPath, merged, frame.seq ?? this.extractSeq(frame.data) ?? this.getInternalSeq(existing)), frameVersion ?? this.getInternalVersion(existing));
|
|
2430
2549
|
this.storage.set(viewPath, frame.key, nextValue);
|
|
2431
2550
|
const update = {
|
|
2432
2551
|
type: 'patch',
|
|
@@ -2445,6 +2564,7 @@ class FrameProcessor {
|
|
|
2445
2564
|
break;
|
|
2446
2565
|
case 'delete':
|
|
2447
2566
|
this.storage.delete(viewPath, frame.key);
|
|
2567
|
+
this.forgetEvicted(viewPath, frame.key);
|
|
2448
2568
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
2449
2569
|
type: 'delete',
|
|
2450
2570
|
key: frame.key,
|
|
@@ -2481,8 +2601,56 @@ class FrameProcessor {
|
|
|
2481
2601
|
return;
|
|
2482
2602
|
while (this.storage.size(viewPath) > this.maxEntriesPerView) {
|
|
2483
2603
|
const evicted = this.storage.evictOldest(viewPath);
|
|
2484
|
-
if (evicted
|
|
2485
|
-
|
|
2604
|
+
if (evicted === undefined)
|
|
2605
|
+
continue;
|
|
2606
|
+
this.queryStore?.evict(viewPath, evicted);
|
|
2607
|
+
this.rememberEvicted(viewPath, evicted, this.maxEntriesPerView);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Remember a key `maxEntriesPerView` dropped, so a later patch for it can be
|
|
2612
|
+
* reported as such. At most `limit` keys per view, most recent kept.
|
|
2613
|
+
*/
|
|
2614
|
+
rememberEvicted(viewPath, key, limit) {
|
|
2615
|
+
let keys = this.evictedKeys.get(viewPath);
|
|
2616
|
+
if (!keys) {
|
|
2617
|
+
keys = new Set();
|
|
2618
|
+
this.evictedKeys.set(viewPath, keys);
|
|
2619
|
+
}
|
|
2620
|
+
keys.delete(key);
|
|
2621
|
+
keys.add(key);
|
|
2622
|
+
while (keys.size > Math.max(limit, 1)) {
|
|
2623
|
+
const oldest = keys.values().next().value;
|
|
2624
|
+
if (oldest === undefined)
|
|
2625
|
+
break;
|
|
2626
|
+
keys.delete(oldest);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
forgetEvicted(viewPath, key) {
|
|
2630
|
+
this.evictedKeys.get(viewPath)?.delete(key);
|
|
2631
|
+
}
|
|
2632
|
+
reportDiscardedPatch(viewPath, frame) {
|
|
2633
|
+
const seq = frame.seq ?? this.extractSeq(frame.data);
|
|
2634
|
+
const evicted = this.evictedKeys.get(viewPath)?.has(frame.key) === true;
|
|
2635
|
+
const error = evicted
|
|
2636
|
+
? new AreteError(`Discarded patch for '${frame.key}' in ${viewPath}: this client dropped the entity to `
|
|
2637
|
+
+ `stay within maxEntriesPerView (${String(this.maxEntriesPerView)}), and the server `
|
|
2638
|
+
+ 'sends no upsert for it until it re-enters the query or the subscription is '
|
|
2639
|
+
+ 're-established; raise maxEntriesPerView above the subscription size', 'PATCH_FOR_EVICTED_KEY')
|
|
2640
|
+
: new AreteError(`Discarded patch for '${frame.key}' in ${viewPath}: the client holds no entity for `
|
|
2641
|
+
+ 'this key; it appears when the server sends a full upsert', 'PATCH_FOR_UNKNOWN_KEY');
|
|
2642
|
+
const diagnostic = {
|
|
2643
|
+
view: viewPath,
|
|
2644
|
+
key: frame.key,
|
|
2645
|
+
...(seq !== undefined ? { seq } : {}),
|
|
2646
|
+
operation: 'patch',
|
|
2647
|
+
reason: evicted ? 'evicted-key' : 'unknown-key',
|
|
2648
|
+
error,
|
|
2649
|
+
};
|
|
2650
|
+
this.reportDiagnostic(diagnostic);
|
|
2651
|
+
if (evicted && this.warnOnValidationError && !this.warnedEvictedViews.has(viewPath)) {
|
|
2652
|
+
this.warnedEvictedViews.add(viewPath);
|
|
2653
|
+
console.warn('[Arete] Patch dropped for a locally evicted entity:', diagnostic);
|
|
2486
2654
|
}
|
|
2487
2655
|
}
|
|
2488
2656
|
}
|
|
@@ -2629,6 +2797,66 @@ class MemoryAdapter {
|
|
|
2629
2797
|
}
|
|
2630
2798
|
}
|
|
2631
2799
|
|
|
2800
|
+
/** Whether a sort value cannot be ranked: it is missing or `null`. */
|
|
2801
|
+
function isUnrankedSortValue(value) {
|
|
2802
|
+
return value === undefined || value === null;
|
|
2803
|
+
}
|
|
2804
|
+
/**
|
|
2805
|
+
* Compares two ranked sort values in ascending order. Numbers and bigints
|
|
2806
|
+
* compare numerically (also against each other), booleans as `false < true`,
|
|
2807
|
+
* and everything else with `localeCompare` on its string form (the SDK
|
|
2808
|
+
* ordering rule, shared with the Rust and Python SDKs).
|
|
2809
|
+
*
|
|
2810
|
+
* This is not the server's ranking. The server compares strings by byte
|
|
2811
|
+
* order, except that two decimal-integer strings compare numerically, and it
|
|
2812
|
+
* ranks numbers below strings and booleans below both. The server's order
|
|
2813
|
+
* decides which entities fill a `take` window; this one only orders the
|
|
2814
|
+
* members the client already holds, so the two can list a window's members
|
|
2815
|
+
* differently, never choose different members.
|
|
2816
|
+
*/
|
|
2817
|
+
function compareSortValues(left, right) {
|
|
2818
|
+
if (left === right)
|
|
2819
|
+
return 0;
|
|
2820
|
+
if (isUnrankedSortValue(left))
|
|
2821
|
+
return isUnrankedSortValue(right) ? 0 : 1;
|
|
2822
|
+
if (isUnrankedSortValue(right))
|
|
2823
|
+
return -1;
|
|
2824
|
+
if (typeof left === 'number' && typeof right === 'number')
|
|
2825
|
+
return left - right;
|
|
2826
|
+
if (typeof left === 'bigint' && typeof right === 'bigint')
|
|
2827
|
+
return left < right ? -1 : 1;
|
|
2828
|
+
if (typeof left === 'bigint' && typeof right === 'number' && Number.isInteger(right)) {
|
|
2829
|
+
const other = BigInt(right);
|
|
2830
|
+
return left === other ? 0 : left < other ? -1 : 1;
|
|
2831
|
+
}
|
|
2832
|
+
if (typeof left === 'number' && typeof right === 'bigint' && Number.isInteger(left)) {
|
|
2833
|
+
const other = BigInt(left);
|
|
2834
|
+
return other === right ? 0 : other < right ? -1 : 1;
|
|
2835
|
+
}
|
|
2836
|
+
if (typeof left === 'boolean' && typeof right === 'boolean')
|
|
2837
|
+
return Number(left) - Number(right);
|
|
2838
|
+
return String(left).localeCompare(String(right));
|
|
2839
|
+
}
|
|
2840
|
+
/**
|
|
2841
|
+
* Orders two entities' sort values for a view sorted in `order`.
|
|
2842
|
+
*
|
|
2843
|
+
* A missing or `null` sort value cannot be ranked, so it sorts after every
|
|
2844
|
+
* ranked value in both directions: `desc` reverses only the comparison between
|
|
2845
|
+
* two ranked values. Two unranked values tie. Callers break ties on the entity
|
|
2846
|
+
* key, ascending, whatever the order. The unranked-last rule is the server's
|
|
2847
|
+
* and the Rust and Python SDKs'; how two ranked values compare is
|
|
2848
|
+
* `compareRanked` (by default {@link compareSortValues}, which differs from
|
|
2849
|
+
* the server as described there).
|
|
2850
|
+
*/
|
|
2851
|
+
function compareSortOrder(left, right, order, compareRanked = compareSortValues) {
|
|
2852
|
+
const leftUnranked = isUnrankedSortValue(left);
|
|
2853
|
+
const rightUnranked = isUnrankedSortValue(right);
|
|
2854
|
+
if (leftUnranked || rightUnranked)
|
|
2855
|
+
return Number(leftUnranked) - Number(rightUnranked);
|
|
2856
|
+
const compared = compareRanked(left, right);
|
|
2857
|
+
return order === 'desc' ? -compared : compared;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2632
2860
|
function getNestedValue$1(obj, path) {
|
|
2633
2861
|
let current = obj;
|
|
2634
2862
|
for (const segment of path) {
|
|
@@ -2640,33 +2868,6 @@ function getNestedValue$1(obj, path) {
|
|
|
2640
2868
|
}
|
|
2641
2869
|
return current;
|
|
2642
2870
|
}
|
|
2643
|
-
function compareSortValues(a, b) {
|
|
2644
|
-
if (a === b)
|
|
2645
|
-
return 0;
|
|
2646
|
-
if (a === undefined || a === null)
|
|
2647
|
-
return -1;
|
|
2648
|
-
if (b === undefined || b === null)
|
|
2649
|
-
return 1;
|
|
2650
|
-
if (typeof a === 'bigint' && typeof b === 'bigint') {
|
|
2651
|
-
return a < b ? -1 : 1;
|
|
2652
|
-
}
|
|
2653
|
-
if (typeof a === 'number' && typeof b === 'number') {
|
|
2654
|
-
return a - b;
|
|
2655
|
-
}
|
|
2656
|
-
if (typeof a === 'bigint' && typeof b === 'number' && Number.isInteger(b)) {
|
|
2657
|
-
return a < BigInt(b) ? -1 : 1;
|
|
2658
|
-
}
|
|
2659
|
-
if (typeof a === 'number' && typeof b === 'bigint' && Number.isInteger(a)) {
|
|
2660
|
-
return BigInt(a) < b ? -1 : 1;
|
|
2661
|
-
}
|
|
2662
|
-
if (typeof a === 'string' && typeof b === 'string') {
|
|
2663
|
-
return a.localeCompare(b);
|
|
2664
|
-
}
|
|
2665
|
-
if (typeof a === 'boolean' && typeof b === 'boolean') {
|
|
2666
|
-
return (a ? 1 : 0) - (b ? 1 : 0);
|
|
2667
|
-
}
|
|
2668
|
-
return String(a).localeCompare(String(b));
|
|
2669
|
-
}
|
|
2670
2871
|
class SortedStorageDecorator {
|
|
2671
2872
|
constructor(inner) {
|
|
2672
2873
|
this.sortConfigs = new Map();
|
|
@@ -2786,7 +2987,6 @@ class SortedStorageDecorator {
|
|
|
2786
2987
|
}
|
|
2787
2988
|
binarySearchInsertPosition(viewPath, sortedKeys, sortConfig, newKey, newValue) {
|
|
2788
2989
|
const newSortValue = getNestedValue$1(newValue, sortConfig.field);
|
|
2789
|
-
const isDesc = sortConfig.order === 'desc';
|
|
2790
2990
|
let low = 0;
|
|
2791
2991
|
let high = sortedKeys.length;
|
|
2792
2992
|
while (low < high) {
|
|
@@ -2794,9 +2994,8 @@ class SortedStorageDecorator {
|
|
|
2794
2994
|
const midKey = sortedKeys[mid];
|
|
2795
2995
|
const midEntity = this.inner.get(viewPath, midKey);
|
|
2796
2996
|
const midValue = getNestedValue$1(midEntity, sortConfig.field);
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
cmp = -cmp;
|
|
2997
|
+
// Unranked (missing or null) values sort last in both directions.
|
|
2998
|
+
let cmp = compareSortOrder(newSortValue, midValue, sortConfig.order);
|
|
2800
2999
|
if (cmp === 0) {
|
|
2801
3000
|
cmp = newKey.localeCompare(midKey);
|
|
2802
3001
|
}
|
|
@@ -2813,14 +3012,11 @@ class SortedStorageDecorator {
|
|
|
2813
3012
|
const allKeys = this.inner.keys(viewPath);
|
|
2814
3013
|
if (allKeys.length === 0)
|
|
2815
3014
|
return;
|
|
2816
|
-
const isDesc = sortConfig.order === 'desc';
|
|
2817
3015
|
const entries = allKeys.map(k => [k, this.inner.get(viewPath, k)]);
|
|
2818
3016
|
entries.sort((a, b) => {
|
|
2819
3017
|
const aValue = getNestedValue$1(a[1], sortConfig.field);
|
|
2820
3018
|
const bValue = getNestedValue$1(b[1], sortConfig.field);
|
|
2821
|
-
let cmp =
|
|
2822
|
-
if (isDesc)
|
|
2823
|
-
cmp = -cmp;
|
|
3019
|
+
let cmp = compareSortOrder(aValue, bValue, sortConfig.order);
|
|
2824
3020
|
if (cmp === 0) {
|
|
2825
3021
|
cmp = a[0].localeCompare(b[0]);
|
|
2826
3022
|
}
|
|
@@ -2839,25 +3035,9 @@ function getNestedValue(value, path) {
|
|
|
2839
3035
|
}
|
|
2840
3036
|
return current;
|
|
2841
3037
|
}
|
|
2842
|
-
function compareValues(left, right) {
|
|
2843
|
-
if (left === right)
|
|
2844
|
-
return 0;
|
|
2845
|
-
if (left === undefined || left === null)
|
|
2846
|
-
return -1;
|
|
2847
|
-
if (right === undefined || right === null)
|
|
2848
|
-
return 1;
|
|
2849
|
-
if (typeof left === 'number' && typeof right === 'number')
|
|
2850
|
-
return left - right;
|
|
2851
|
-
if (typeof left === 'bigint' && typeof right === 'bigint')
|
|
2852
|
-
return left < right ? -1 : 1;
|
|
2853
|
-
if (typeof left === 'boolean' && typeof right === 'boolean') {
|
|
2854
|
-
return Number(left) - Number(right);
|
|
2855
|
-
}
|
|
2856
|
-
return String(left).localeCompare(String(right));
|
|
2857
|
-
}
|
|
2858
3038
|
function compareSequences(left, right) {
|
|
2859
3039
|
if (typeof left !== 'string' || typeof right !== 'string')
|
|
2860
|
-
return
|
|
3040
|
+
return compareSortValues(left, right);
|
|
2861
3041
|
const [leftSlot, leftIndex = ''] = left.split(':', 2);
|
|
2862
3042
|
const [rightSlot, rightIndex = ''] = right.split(':', 2);
|
|
2863
3043
|
if (/^\d+$/.test(leftSlot ?? '') && /^\d+$/.test(rightSlot ?? '')) {
|
|
@@ -3149,9 +3329,9 @@ class QueryStore {
|
|
|
3149
3329
|
const right = field
|
|
3150
3330
|
? getNestedValue(this.storage.get(view, rightKey), field)
|
|
3151
3331
|
: record.sequences.get(rightKey);
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3332
|
+
// A missing or null sort value (or sequence) sorts after every ranked
|
|
3333
|
+
// one in both directions; ties break on the key, always ascending.
|
|
3334
|
+
const compared = compareSortOrder(left, right, order, field ? compareSortValues : compareSequences);
|
|
3155
3335
|
return compared === 0 ? leftKey.localeCompare(rightKey) : compared;
|
|
3156
3336
|
});
|
|
3157
3337
|
}
|
|
@@ -3409,10 +3589,90 @@ function createRichUpdateStream(_storage, subscriptionRegistry, query, keyFilter
|
|
|
3409
3589
|
};
|
|
3410
3590
|
}
|
|
3411
3591
|
|
|
3592
|
+
/** How long `get` and `getOne` wait for an initial snapshot, as in the Rust and Python SDKs. */
|
|
3593
|
+
const DEFAULT_INITIAL_DATA_TIMEOUT_MS = 5000;
|
|
3594
|
+
/**
|
|
3595
|
+
* `get` or `getOne` gave up waiting for the initial snapshot: the socket never
|
|
3596
|
+
* delivered it (no connection, a reconnect storm, a server that never
|
|
3597
|
+
* answers). The read's subscription is released before this is thrown.
|
|
3598
|
+
*/
|
|
3599
|
+
class InitialDataTimeoutError extends AreteError {
|
|
3600
|
+
constructor(view, timeoutMs) {
|
|
3601
|
+
super(`Timed out after ${timeoutMs}ms waiting for the initial snapshot of view '${view}'`, 'INITIAL_DATA_TIMEOUT', { view, timeoutMs });
|
|
3602
|
+
this.view = view;
|
|
3603
|
+
this.timeoutMs = timeoutMs;
|
|
3604
|
+
this.name = 'InitialDataTimeoutError';
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3412
3607
|
function queryOptions(options) {
|
|
3413
|
-
const { schema: _schema, withSnapshot, ...query } = options ?? {};
|
|
3608
|
+
const { schema: _schema, withSnapshot, timeoutMs: _timeoutMs, ...query } = options ?? {};
|
|
3414
3609
|
return { query, snapshotEnabled: withSnapshot ?? true };
|
|
3415
3610
|
}
|
|
3611
|
+
function resolveTimeout(options) {
|
|
3612
|
+
const timeoutMs = options?.timeoutMs === undefined
|
|
3613
|
+
? DEFAULT_INITIAL_DATA_TIMEOUT_MS
|
|
3614
|
+
: options.timeoutMs;
|
|
3615
|
+
if (timeoutMs !== null && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
|
|
3616
|
+
throw new RangeError('timeoutMs must be a positive finite number or null');
|
|
3617
|
+
}
|
|
3618
|
+
return timeoutMs;
|
|
3619
|
+
}
|
|
3620
|
+
/**
|
|
3621
|
+
* Open (or reuse) the equivalent subscription, wait until its snapshot has
|
|
3622
|
+
* resolved or its query has failed, and release it. A terminal connection
|
|
3623
|
+
* failure or a disconnect rejects with that reason. Bounded by `timeoutMs`
|
|
3624
|
+
* (`null` waits forever) so a socket that never delivers cannot hang the caller.
|
|
3625
|
+
*/
|
|
3626
|
+
async function readResolved(registry, query, snapshotEnabled, timeoutMs) {
|
|
3627
|
+
const lease = registry.subscribe(query, snapshotEnabled);
|
|
3628
|
+
try {
|
|
3629
|
+
await new Promise((resolve, reject) => {
|
|
3630
|
+
let settled = false;
|
|
3631
|
+
let stop = () => { };
|
|
3632
|
+
let timer;
|
|
3633
|
+
const finish = (error) => {
|
|
3634
|
+
if (settled)
|
|
3635
|
+
return;
|
|
3636
|
+
settled = true;
|
|
3637
|
+
stop();
|
|
3638
|
+
if (timer !== undefined)
|
|
3639
|
+
clearTimeout(timer);
|
|
3640
|
+
if (error)
|
|
3641
|
+
reject(error);
|
|
3642
|
+
else
|
|
3643
|
+
resolve();
|
|
3644
|
+
};
|
|
3645
|
+
const check = () => {
|
|
3646
|
+
const error = lease.getError();
|
|
3647
|
+
if (error)
|
|
3648
|
+
finish(error);
|
|
3649
|
+
else if (!lease.getSnapshot().isLoading)
|
|
3650
|
+
finish();
|
|
3651
|
+
else {
|
|
3652
|
+
const connectionError = registry.getConnectionError();
|
|
3653
|
+
if (connectionError)
|
|
3654
|
+
finish(connectionError);
|
|
3655
|
+
}
|
|
3656
|
+
};
|
|
3657
|
+
check();
|
|
3658
|
+
if (settled)
|
|
3659
|
+
return;
|
|
3660
|
+
const stopChanges = lease.onChange(check);
|
|
3661
|
+
const stopFailures = registry.onFailure(finish);
|
|
3662
|
+
stop = () => {
|
|
3663
|
+
stopChanges();
|
|
3664
|
+
stopFailures();
|
|
3665
|
+
};
|
|
3666
|
+
if (timeoutMs !== null) {
|
|
3667
|
+
timer = setTimeout(() => finish(new InitialDataTimeoutError(query.view, timeoutMs)), timeoutMs);
|
|
3668
|
+
}
|
|
3669
|
+
});
|
|
3670
|
+
return lease.getSnapshot();
|
|
3671
|
+
}
|
|
3672
|
+
finally {
|
|
3673
|
+
lease.release();
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3416
3676
|
function serializeViewKeyValue(value, view, field) {
|
|
3417
3677
|
const location = field === undefined ? `view '${view}'` : `key field '${field}' for view '${view}'`;
|
|
3418
3678
|
if (typeof value === 'string')
|
|
@@ -3463,7 +3723,10 @@ function createTypedStateView(viewDef, storage, subscriptionRegistry) {
|
|
|
3463
3723
|
return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key: serializedKey, ...query }, serializedKey, snapshotEnabled);
|
|
3464
3724
|
},
|
|
3465
3725
|
async get(key, options) {
|
|
3466
|
-
|
|
3726
|
+
const timeoutMs = resolveTimeout(options);
|
|
3727
|
+
const { query, snapshotEnabled } = queryOptions(options);
|
|
3728
|
+
const snapshot = await readResolved(subscriptionRegistry, { view: viewDef.view, key: wireKey(key), ...query }, snapshotEnabled, timeoutMs);
|
|
3729
|
+
return snapshot.data[0] ?? null;
|
|
3467
3730
|
},
|
|
3468
3731
|
getSync(key, options) {
|
|
3469
3732
|
const { query, snapshotEnabled } = queryOptions(options);
|
|
@@ -3491,7 +3754,14 @@ function createTypedListView(viewDef, storage, subscriptionRegistry) {
|
|
|
3491
3754
|
return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...query }, undefined, snapshotEnabled);
|
|
3492
3755
|
},
|
|
3493
3756
|
async get(options) {
|
|
3494
|
-
|
|
3757
|
+
const timeoutMs = resolveTimeout(options);
|
|
3758
|
+
const { query, snapshotEnabled } = queryOptions(options);
|
|
3759
|
+
const snapshot = await readResolved(subscriptionRegistry, { view: viewDef.view, ...query }, snapshotEnabled, timeoutMs);
|
|
3760
|
+
return [...snapshot.data];
|
|
3761
|
+
},
|
|
3762
|
+
async getOne(options) {
|
|
3763
|
+
const items = await this.get({ ...options, take: 1 });
|
|
3764
|
+
return items[0] ?? null;
|
|
3495
3765
|
},
|
|
3496
3766
|
getSync(options) {
|
|
3497
3767
|
const { query, snapshotEnabled } = queryOptions(options);
|
|
@@ -6227,16 +6497,22 @@ const PROGRAM_READ_DESCRIPTOR = Symbol.for('@usearete/sdk/program-read-descripto
|
|
|
6227
6497
|
/**
|
|
6228
6498
|
* Bundle a generated program definition with its default read descriptor.
|
|
6229
6499
|
*
|
|
6230
|
-
* Generated SDK entries call this automatically. The descriptor
|
|
6231
|
-
*
|
|
6500
|
+
* Generated SDK entries call this automatically. The descriptor lives under a
|
|
6501
|
+
* registry symbol, so `Object.keys` and `JSON.stringify` never see it, while
|
|
6502
|
+
* `{ ...program }` keeps it: a spread program still reads through its release.
|
|
6503
|
+
* Every other own property (including the runtime extension carrier) is copied
|
|
6504
|
+
* unchanged, except `packageReleaseHash`: the result reads differently from
|
|
6505
|
+
* the generated program SDK, so it is no longer provably that SDK. Generated
|
|
6506
|
+
* entries apply `withProgramIdentity` last.
|
|
6232
6507
|
*/
|
|
6233
6508
|
function withProgramRead(program, descriptor) {
|
|
6234
6509
|
const properties = Object.getOwnPropertyDescriptors(program);
|
|
6235
6510
|
Reflect.deleteProperty(properties, PROGRAM_READ_DESCRIPTOR);
|
|
6511
|
+
Reflect.deleteProperty(properties, 'packageReleaseHash');
|
|
6236
6512
|
const bundled = Object.create(Object.getPrototypeOf(program), properties);
|
|
6237
6513
|
Object.defineProperty(bundled, PROGRAM_READ_DESCRIPTOR, {
|
|
6238
6514
|
value: descriptor,
|
|
6239
|
-
enumerable:
|
|
6515
|
+
enumerable: true,
|
|
6240
6516
|
configurable: false,
|
|
6241
6517
|
writable: false,
|
|
6242
6518
|
});
|
|
@@ -6246,9 +6522,62 @@ function withProgramRead(program, descriptor) {
|
|
|
6246
6522
|
function getProgramReadDescriptor(program) {
|
|
6247
6523
|
return program?.[PROGRAM_READ_DESCRIPTOR];
|
|
6248
6524
|
}
|
|
6525
|
+
/**
|
|
6526
|
+
* Stamp a program definition with the identity of the program SDK it is.
|
|
6527
|
+
*
|
|
6528
|
+
* Generated program SDK entries call this last, after the package's own
|
|
6529
|
+
* extension and read descriptor are applied, so the identity describes
|
|
6530
|
+
* exactly the generated SDK. `extendProgram`, `extendPrograms` and
|
|
6531
|
+
* `withProgramRead` drop the identity again: a program changed outside its
|
|
6532
|
+
* generated SDK is no longer provably that SDK. Every other own property,
|
|
6533
|
+
* including symbol-keyed runtime extensions and the read descriptor, is copied
|
|
6534
|
+
* unchanged. An absent or empty `packageReleaseHash` removes the identity.
|
|
6535
|
+
*/
|
|
6536
|
+
function withProgramIdentity(program, identity) {
|
|
6537
|
+
const properties = Object.getOwnPropertyDescriptors(program);
|
|
6538
|
+
Reflect.deleteProperty(properties, 'packageReleaseHash');
|
|
6539
|
+
const stamped = Object.create(Object.getPrototypeOf(program), properties);
|
|
6540
|
+
if (typeof identity.packageReleaseHash === 'string' && identity.packageReleaseHash.length > 0) {
|
|
6541
|
+
Object.defineProperty(stamped, 'packageReleaseHash', {
|
|
6542
|
+
value: identity.packageReleaseHash,
|
|
6543
|
+
enumerable: true,
|
|
6544
|
+
configurable: true,
|
|
6545
|
+
writable: true,
|
|
6546
|
+
});
|
|
6547
|
+
}
|
|
6548
|
+
return stamped;
|
|
6549
|
+
}
|
|
6249
6550
|
|
|
6250
|
-
|
|
6251
|
-
|
|
6551
|
+
/**
|
|
6552
|
+
* Version of the extension contract: `defineStackExtensions`,
|
|
6553
|
+
* `defineProgramExtensions`, `extendProgram`, `extendPrograms`, `extendStack`,
|
|
6554
|
+
* `withProgramRead`, and the instruction helpers generated code imports
|
|
6555
|
+
* (`createInstructionHandler`, `buildInstruction`, `instructionOperation`,
|
|
6556
|
+
* `createPreparedInstruction`, the PDA and read helpers, …).
|
|
6557
|
+
*
|
|
6558
|
+
* It only changes on a breaking change to that contract, so generated SDKs and
|
|
6559
|
+
* extensions built for one value keep working with every SDK release that
|
|
6560
|
+
* exports the same value. The same number is published in this package's
|
|
6561
|
+
* `package.json` as `arete.extensionApi` for tools that read `node_modules`.
|
|
6562
|
+
*/
|
|
6563
|
+
const EXTENSION_API_VERSION = 1;
|
|
6564
|
+
/**
|
|
6565
|
+
* Runtime extension carriers live under registry symbols (`Symbol.for`), so
|
|
6566
|
+
* two copies of this module still read each other's definitions. The
|
|
6567
|
+
* properties are enumerable: `{ ...stack }` and `{ ...program }` keep them,
|
|
6568
|
+
* while `Object.keys`, `for…in` and `JSON.stringify` never see a symbol key.
|
|
6569
|
+
*
|
|
6570
|
+
* The constants are typed as their historical property names rather than as
|
|
6571
|
+
* `unique symbol`. Generated program SDKs use them as computed keys inside
|
|
6572
|
+
* object literals, and TypeScript can only name a `unique symbol` key in
|
|
6573
|
+
* emitted declarations from a file that imports it; a string-literal key type
|
|
6574
|
+
* keeps every generated `.d.ts` nameable and identical across module copies.
|
|
6575
|
+
* Always read and write these properties through the constants (or
|
|
6576
|
+
* `getStackRuntimeExtensions` / `getProgramRuntimeExtensions`), never by the
|
|
6577
|
+
* literal name.
|
|
6578
|
+
*/
|
|
6579
|
+
const STACK_RUNTIME_EXTENSIONS = Symbol.for('@usearete/sdk/stack-runtime-extensions');
|
|
6580
|
+
const PROGRAM_OPERATION_EXTENSIONS = Symbol.for('@usearete/sdk/program-runtime-extensions');
|
|
6252
6581
|
function defineProgramExtensions() {
|
|
6253
6582
|
return (extensions) => extensions;
|
|
6254
6583
|
}
|
|
@@ -6277,7 +6606,12 @@ function extendProgram(program, extensions) {
|
|
|
6277
6606
|
if (readDescriptor) {
|
|
6278
6607
|
Object.defineProperty(extended, PROGRAM_READ_DESCRIPTOR, readDescriptor);
|
|
6279
6608
|
}
|
|
6609
|
+
// The extended program is no longer byte-for-byte the generated one, nor
|
|
6610
|
+
// provably the program SDK its package release describes: extensions
|
|
6611
|
+
// applied outside generated code must not borrow that identity. Generated
|
|
6612
|
+
// entries stamp it back with `withProgramIdentity` after their own.
|
|
6280
6613
|
delete extended.sdkDefinitionHash;
|
|
6614
|
+
delete extended.packageReleaseHash;
|
|
6281
6615
|
for (const key of ['pdas', 'accounts', 'queries', 'addresses', 'constants', 'defaults', 'math']) {
|
|
6282
6616
|
const extensionValue = extensions[key];
|
|
6283
6617
|
if (extensionValue !== undefined) {
|
|
@@ -6313,7 +6647,8 @@ function extendProgram(program, extensions) {
|
|
|
6313
6647
|
return mergeNamespace(baseRead, extensionReadFactory?.(context));
|
|
6314
6648
|
},
|
|
6315
6649
|
},
|
|
6316
|
-
|
|
6650
|
+
// Enumerable so spreading the extended program keeps its operations.
|
|
6651
|
+
enumerable: true,
|
|
6317
6652
|
configurable: false,
|
|
6318
6653
|
writable: false,
|
|
6319
6654
|
});
|
|
@@ -6358,7 +6693,8 @@ function extendStack(stack, extensions) {
|
|
|
6358
6693
|
? (client) => mergeNamespace(baseRuntime.createFlows(client), extensions.createFlows(client))
|
|
6359
6694
|
: extensions.createFlows ?? baseRuntime?.createFlows,
|
|
6360
6695
|
},
|
|
6361
|
-
|
|
6696
|
+
// Enumerable so `{ ...stack }` keeps `read`, `flows` and `readArgCounts`.
|
|
6697
|
+
enumerable: true,
|
|
6362
6698
|
configurable: false,
|
|
6363
6699
|
writable: false,
|
|
6364
6700
|
});
|
|
@@ -6396,7 +6732,7 @@ function isPreparedInstruction(instruction) {
|
|
|
6396
6732
|
&& instruction.kind === 'instruction'
|
|
6397
6733
|
&& 'instruction' in instruction;
|
|
6398
6734
|
}
|
|
6399
|
-
function nonEmpty(values, label) {
|
|
6735
|
+
function nonEmpty$1(values, label) {
|
|
6400
6736
|
if (values.length === 0) {
|
|
6401
6737
|
throw new Error(`${label} must contain at least one item`);
|
|
6402
6738
|
}
|
|
@@ -6409,7 +6745,7 @@ function inferSignerAddresses(instructions) {
|
|
|
6409
6745
|
return dedupe(instructions.flatMap((instruction) => instruction.keys.filter((key) => key.isSigner).map((key) => key.pubkey)));
|
|
6410
6746
|
}
|
|
6411
6747
|
function createPreparedTransactionBody(input) {
|
|
6412
|
-
const instructions = nonEmpty(input.instructions, `Transaction '${input.name}'`);
|
|
6748
|
+
const instructions = nonEmpty$1(input.instructions, `Transaction '${input.name}'`);
|
|
6413
6749
|
return {
|
|
6414
6750
|
name: input.name,
|
|
6415
6751
|
instructions,
|
|
@@ -6422,7 +6758,7 @@ function createPlan(name, artifacts, transactions) {
|
|
|
6422
6758
|
return {
|
|
6423
6759
|
name,
|
|
6424
6760
|
artifacts,
|
|
6425
|
-
transactions: nonEmpty(transactions, `Flow '${name}'`),
|
|
6761
|
+
transactions: nonEmpty$1(transactions, `Flow '${name}'`),
|
|
6426
6762
|
};
|
|
6427
6763
|
}
|
|
6428
6764
|
function createPreparedInstruction(input) {
|
|
@@ -6708,8 +7044,8 @@ async function executePreparedOperation(host, operation, options = {}) {
|
|
|
6708
7044
|
kind: 'flow',
|
|
6709
7045
|
operationName: operation.name,
|
|
6710
7046
|
artifacts: operation.artifacts,
|
|
6711
|
-
signatures: nonEmpty(receipts.map((receipt) => receipt.signature), `Operation '${operation.name}' signatures`),
|
|
6712
|
-
transactions: nonEmpty(receipts, `Operation '${operation.name}' receipts`),
|
|
7047
|
+
signatures: nonEmpty$1(receipts.map((receipt) => receipt.signature), `Operation '${operation.name}' signatures`),
|
|
7048
|
+
transactions: nonEmpty$1(receipts, `Operation '${operation.name}' receipts`),
|
|
6713
7049
|
...callbackErrorResult,
|
|
6714
7050
|
};
|
|
6715
7051
|
}
|
|
@@ -6985,6 +7321,127 @@ function createProgramReadTransport(config) {
|
|
|
6985
7321
|
};
|
|
6986
7322
|
}
|
|
6987
7323
|
|
|
7324
|
+
/** Error code for two different program SDKs claiming one program key. */
|
|
7325
|
+
const PROGRAM_KEY_CONFLICT = 'PROGRAM_KEY_CONFLICT';
|
|
7326
|
+
function nonEmpty(value) {
|
|
7327
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
7328
|
+
}
|
|
7329
|
+
/** Compare two program definitions by identity, never by name. */
|
|
7330
|
+
function compareProgramIdentity(left, right) {
|
|
7331
|
+
if (left === right)
|
|
7332
|
+
return 'same';
|
|
7333
|
+
const leftRelease = nonEmpty(left.packageReleaseHash);
|
|
7334
|
+
const rightRelease = nonEmpty(right.packageReleaseHash);
|
|
7335
|
+
if (leftRelease !== undefined && rightRelease !== undefined) {
|
|
7336
|
+
return leftRelease === rightRelease ? 'same' : 'different';
|
|
7337
|
+
}
|
|
7338
|
+
const leftSpec = nonEmpty(left.programSpecHash);
|
|
7339
|
+
return leftSpec !== undefined && leftSpec === nonEmpty(right.programSpecHash)
|
|
7340
|
+
? 'unproven'
|
|
7341
|
+
: 'different';
|
|
7342
|
+
}
|
|
7343
|
+
/**
|
|
7344
|
+
* Whether two program definitions are provably the same program SDK: the same
|
|
7345
|
+
* object, or both carry the same `packageReleaseHash` (the program package
|
|
7346
|
+
* release they were generated from). Anything else is not provably the same,
|
|
7347
|
+
* even under the same name.
|
|
7348
|
+
*/
|
|
7349
|
+
function isSameProgramSdk(left, right) {
|
|
7350
|
+
return compareProgramIdentity(left, right) === 'same';
|
|
7351
|
+
}
|
|
7352
|
+
function identityOf(program) {
|
|
7353
|
+
const release = nonEmpty(program.packageReleaseHash);
|
|
7354
|
+
if (release !== undefined)
|
|
7355
|
+
return `package release ${release}`;
|
|
7356
|
+
const spec = nonEmpty(program.programSpecHash);
|
|
7357
|
+
return spec !== undefined
|
|
7358
|
+
? `program spec ${spec}, no package release`
|
|
7359
|
+
: 'no program identity';
|
|
7360
|
+
}
|
|
7361
|
+
const warned = new Set();
|
|
7362
|
+
/** Emit a program identity warning once per distinct message. */
|
|
7363
|
+
function warnOnce(message) {
|
|
7364
|
+
if (warned.has(message))
|
|
7365
|
+
return;
|
|
7366
|
+
warned.add(message);
|
|
7367
|
+
console.warn(`[Arete] ${message}`);
|
|
7368
|
+
}
|
|
7369
|
+
/**
|
|
7370
|
+
* A program attached next to a stack (`withPrograms`, `ConnectOptions.programs`,
|
|
7371
|
+
* `useArete(stack, { programs })`, a session member's `programs`) under a key
|
|
7372
|
+
* the stack already provides with a different program SDK.
|
|
7373
|
+
*/
|
|
7374
|
+
function attachedProgramKeyConflict(stackName, key, stackProgram, attached) {
|
|
7375
|
+
return new AreteError(`Program key '${key}' conflicts with stack '${stackName}': the stack already provides `
|
|
7376
|
+
+ `a different '${key}' program SDK (stack: ${identityOf(stackProgram)}; attached: `
|
|
7377
|
+
+ `${identityOf(attached)}). Use the stack's program at programs.${key}, or attach the `
|
|
7378
|
+
+ `other program under a different key, one the stack does not use.`, PROGRAM_KEY_CONFLICT, { key, stack: stackName });
|
|
7379
|
+
}
|
|
7380
|
+
/**
|
|
7381
|
+
* An attached program replaces the stack's program under one key without a
|
|
7382
|
+
* provable identity match (same program spec, no shared package release).
|
|
7383
|
+
*/
|
|
7384
|
+
function warnUnprovenAttachedProgram(stackName, key, stackProgram, attached) {
|
|
7385
|
+
warnOnce(`programs.${key} uses the program attached to stack '${stackName}': it has the same `
|
|
7386
|
+
+ `program spec as the stack's '${key}' program but could not be proven identical `
|
|
7387
|
+
+ `(stack: ${identityOf(stackProgram)}; attached: ${identityOf(attached)}).`);
|
|
7388
|
+
}
|
|
7389
|
+
/** A standalone session program under a key a session stack already provides. */
|
|
7390
|
+
function sessionProgramKeyConflict(stackKey, key, stackProgram, standalone) {
|
|
7391
|
+
return new AreteError(`Program key '${key}' conflicts with stack '${stackKey}' in this session: the stack `
|
|
7392
|
+
+ `already provides a different '${key}' program SDK (stack: ${identityOf(stackProgram)}; `
|
|
7393
|
+
+ `standalone: ${identityOf(standalone)}). Use session.stacks.${stackKey}.programs.${key}, `
|
|
7394
|
+
+ `or attach the standalone program under a different key in createSession({ programs }).`, PROGRAM_KEY_CONFLICT, { key, stack: stackKey });
|
|
7395
|
+
}
|
|
7396
|
+
/**
|
|
7397
|
+
* A standalone session program takes the top-level key from stacks whose
|
|
7398
|
+
* program it could not be proven identical to: a different identity
|
|
7399
|
+
* (`'identity'`), or the same program SDK read through a different program
|
|
7400
|
+
* read configuration than the stack's (`'program-read'`).
|
|
7401
|
+
*/
|
|
7402
|
+
function warnUnprovenSessionProgram(key, standalone, providers, cause = 'identity') {
|
|
7403
|
+
const stacks = providers.map(({ stackKey }) => `'${stackKey}'`).join(' and ');
|
|
7404
|
+
const paths = providers
|
|
7405
|
+
.map(({ stackKey }) => `session.stacks.${stackKey}.programs.${key}`)
|
|
7406
|
+
.join(' and ');
|
|
7407
|
+
const identities = `standalone: ${identityOf(standalone)}; ${providers
|
|
7408
|
+
.map(({ stackKey, program }) => `${stackKey}: ${identityOf(program)}`)
|
|
7409
|
+
.join('; ')}`;
|
|
7410
|
+
const reason = cause === 'program-read'
|
|
7411
|
+
? `it is the same program SDK as the '${key}' program of stack ${stacks} (${identities}) `
|
|
7412
|
+
+ `but its programReads.${key} descriptor differs from the one the stack reads through, `
|
|
7413
|
+
+ `so it keeps its own program read configuration`
|
|
7414
|
+
: `it has the same program spec as the '${key}' program of stack ${stacks} but could not `
|
|
7415
|
+
+ `be proven identical (${identities})`;
|
|
7416
|
+
warnOnce(`session.programs.${key} uses the standalone program: ${reason}. `
|
|
7417
|
+
+ `${paths} keep${providers.length === 1 ? 's' : ''} the stack's program.`);
|
|
7418
|
+
}
|
|
7419
|
+
/**
|
|
7420
|
+
* Several session stacks bundle one key with the same program spec, not all
|
|
7421
|
+
* provably the same SDK; the first stack's program takes the top-level key.
|
|
7422
|
+
*/
|
|
7423
|
+
function warnUnprovenStackPrograms(key, providers) {
|
|
7424
|
+
const first = providers[0];
|
|
7425
|
+
warnOnce(`session.programs.${key} uses stack '${first.stackKey}''s program: stacks `
|
|
7426
|
+
+ `${providers.map(({ stackKey }) => `'${stackKey}'`).join(' and ')} bundle '${key}' `
|
|
7427
|
+
+ `programs with the same program spec that could not be proven identical (${providers
|
|
7428
|
+
.map(({ stackKey, program }) => `${stackKey}: ${identityOf(program)}`)
|
|
7429
|
+
.join('; ')}). Each stays at session.stacks.<stack>.programs.${key}.`);
|
|
7430
|
+
}
|
|
7431
|
+
/** Two session stacks provide different program SDKs under one key. */
|
|
7432
|
+
function ambiguousSessionProgram(key, providers) {
|
|
7433
|
+
const stacks = providers.map(({ stackKey }) => `'${stackKey}'`).join(' and ');
|
|
7434
|
+
const paths = providers
|
|
7435
|
+
.map(({ stackKey }) => `session.stacks.${stackKey}.programs.${key}`)
|
|
7436
|
+
.join(' or ');
|
|
7437
|
+
const identities = providers
|
|
7438
|
+
.map(({ stackKey, program }) => `${stackKey}: ${identityOf(program)}`)
|
|
7439
|
+
.join('; ');
|
|
7440
|
+
return new AreteError(`session.programs.${key} is ambiguous: stacks ${stacks} provide different '${key}' `
|
|
7441
|
+
+ `program SDKs (${identities}). Use ${paths}, or attach the one you want under a `
|
|
7442
|
+
+ `different key in createSession({ programs }).`, PROGRAM_KEY_CONFLICT, { key, stacks: providers.map(({ stackKey }) => stackKey) });
|
|
7443
|
+
}
|
|
7444
|
+
|
|
6988
7445
|
function effectiveProgramRead(stack, name, override) {
|
|
6989
7446
|
return override
|
|
6990
7447
|
?? stack.programReads?.[name]
|
|
@@ -7092,13 +7549,38 @@ function validateLocalProgramReadEndpoints(stack, overrides, connectHttpUrl) {
|
|
|
7092
7549
|
function hasProgramAccountReads(definition) {
|
|
7093
7550
|
return Object.keys(definition.accounts ?? {}).length > 0;
|
|
7094
7551
|
}
|
|
7552
|
+
/**
|
|
7553
|
+
* Attach programs next to a stack's own, matching a key the stack already
|
|
7554
|
+
* provides by program identity (see `compareProgramIdentity`):
|
|
7555
|
+
*
|
|
7556
|
+
* - the same program SDK (same object, or equal `packageReleaseHash`) resolves
|
|
7557
|
+
* to the stack's definition;
|
|
7558
|
+
* - the same `programSpecHash` without a provable identity match (at least one
|
|
7559
|
+
* side has no `packageReleaseHash`) uses the attached program, with one
|
|
7560
|
+
* warning;
|
|
7561
|
+
* - anything else throws `PROGRAM_KEY_CONFLICT`.
|
|
7562
|
+
*/
|
|
7095
7563
|
function mergeAttachedPrograms(stack, attachedPrograms) {
|
|
7096
7564
|
const merged = { ...(attachedPrograms ?? {}) };
|
|
7097
7565
|
for (const [name, definition] of Object.entries(stack.programs ?? {})) {
|
|
7098
|
-
|
|
7099
|
-
|
|
7566
|
+
const attached = attachedPrograms && Object.prototype.hasOwnProperty.call(attachedPrograms, name)
|
|
7567
|
+
? attachedPrograms[name]
|
|
7568
|
+
: undefined;
|
|
7569
|
+
if (attached === undefined) {
|
|
7570
|
+
merged[name] = definition;
|
|
7571
|
+
continue;
|
|
7572
|
+
}
|
|
7573
|
+
switch (compareProgramIdentity(definition, attached)) {
|
|
7574
|
+
case 'same':
|
|
7575
|
+
merged[name] = definition;
|
|
7576
|
+
break;
|
|
7577
|
+
case 'unproven':
|
|
7578
|
+
warnUnprovenAttachedProgram(stack.name, name, definition, attached);
|
|
7579
|
+
merged[name] = attached;
|
|
7580
|
+
break;
|
|
7581
|
+
case 'different':
|
|
7582
|
+
throw attachedProgramKeyConflict(stack.name, name, definition, attached);
|
|
7100
7583
|
}
|
|
7101
|
-
merged[name] = definition;
|
|
7102
7584
|
}
|
|
7103
7585
|
return merged;
|
|
7104
7586
|
}
|
|
@@ -7197,8 +7679,8 @@ let Arete$1 = class Arete {
|
|
|
7197
7679
|
this.connection.onFrame((frame) => {
|
|
7198
7680
|
this.processor.handleFrame(frame);
|
|
7199
7681
|
});
|
|
7200
|
-
this.connection.onStateChange((state) => {
|
|
7201
|
-
this.subscriptionRegistry.handleConnectionState(state);
|
|
7682
|
+
this.connection.onStateChange((state, error) => {
|
|
7683
|
+
this.subscriptionRegistry.handleConnectionState(state, error);
|
|
7202
7684
|
});
|
|
7203
7685
|
this._views = createTypedViews(this.stack, this.storage, this.subscriptionRegistry);
|
|
7204
7686
|
this._queries = this.buildQueries();
|
|
@@ -7698,6 +8180,34 @@ function resolveSessionProgramReads(stack, member, options) {
|
|
|
7698
8180
|
}));
|
|
7699
8181
|
return Object.keys(reads).length > 0 ? reads : undefined;
|
|
7700
8182
|
}
|
|
8183
|
+
/**
|
|
8184
|
+
* The program read descriptor a session member connected for `stack` reads
|
|
8185
|
+
* `programKey` through, resolved the way `Arete.connect` resolves it: session
|
|
8186
|
+
* and member overrides, then the stack's `programReads`, then the descriptor
|
|
8187
|
+
* the program carries.
|
|
8188
|
+
*/
|
|
8189
|
+
function memberProgramRead(stack, programKey, member, options) {
|
|
8190
|
+
return resolveSessionProgramReads(stack, member, options)?.[programKey]
|
|
8191
|
+
?? stack.programReads?.[programKey]
|
|
8192
|
+
?? getProgramReadDescriptor(stack.programs?.[programKey]);
|
|
8193
|
+
}
|
|
8194
|
+
/** Structural equality of two program read descriptors (plain JSON data). */
|
|
8195
|
+
function sameProgramRead(left, right) {
|
|
8196
|
+
if (left === right)
|
|
8197
|
+
return true;
|
|
8198
|
+
if (left === null || right === null
|
|
8199
|
+
|| typeof left !== 'object' || typeof right !== 'object'
|
|
8200
|
+
|| Array.isArray(left) !== Array.isArray(right)) {
|
|
8201
|
+
return false;
|
|
8202
|
+
}
|
|
8203
|
+
const leftRecord = left;
|
|
8204
|
+
const rightRecord = right;
|
|
8205
|
+
const leftKeys = Object.keys(leftRecord).filter((key) => leftRecord[key] !== undefined);
|
|
8206
|
+
const rightKeys = Object.keys(rightRecord).filter((key) => rightRecord[key] !== undefined);
|
|
8207
|
+
return leftKeys.length === rightKeys.length
|
|
8208
|
+
&& leftKeys.every((key) => Object.prototype.hasOwnProperty.call(rightRecord, key)
|
|
8209
|
+
&& sameProgramRead(leftRecord[key], rightRecord[key]));
|
|
8210
|
+
}
|
|
7701
8211
|
function resolveMemberConnectOptions(stack, member, options, forceHttpOnly) {
|
|
7702
8212
|
const transport = member?.transport ?? options?.transport ?? (forceHttpOnly ? 'http' : 'ws');
|
|
7703
8213
|
// Stack endpoints remain on the stack definition. Only explicit session/member
|
|
@@ -7791,11 +8301,72 @@ async function createSession(definition, ...args) {
|
|
|
7791
8301
|
validateCompositionProgramReads(programAsStack(key, program, definition.programReads?.[key]), options?.programs?.[key], options);
|
|
7792
8302
|
}
|
|
7793
8303
|
}
|
|
8304
|
+
// Program identity, resolved before anything connects. Every stack's
|
|
8305
|
+
// effective programs (its own plus member-attached ones, matched by
|
|
8306
|
+
// `withPrograms`) are the providers for each key.
|
|
8307
|
+
const effectiveStacks = new Map(stackEntries.map(([key, stack]) => {
|
|
8308
|
+
const memberOptions = options?.stacks?.[key];
|
|
8309
|
+
return [key, withPrograms(stack, memberOptions?.programs)];
|
|
8310
|
+
}));
|
|
8311
|
+
const stackProviders = new Map();
|
|
8312
|
+
for (const [stackKey, stack] of effectiveStacks) {
|
|
8313
|
+
for (const [programKey, program] of Object.entries(stack.programs ?? {})) {
|
|
8314
|
+
const providers = stackProviders.get(programKey) ?? [];
|
|
8315
|
+
providers.push({ stackKey, program });
|
|
8316
|
+
stackProviders.set(programKey, providers);
|
|
8317
|
+
}
|
|
8318
|
+
}
|
|
8319
|
+
// Standalone programs a stack already provides as the same program SDK, read
|
|
8320
|
+
// through the same program read configuration, are served by that stack's
|
|
8321
|
+
// connected instance instead of a second member. One with the same program
|
|
8322
|
+
// spec but no provable identity match, or the same program SDK with its own
|
|
8323
|
+
// `programReads` descriptor that differs from the stack's, gets its own
|
|
8324
|
+
// member and takes session.programs.<key>; the stacks keep theirs.
|
|
8325
|
+
const sharedStandalonePrograms = new Set();
|
|
8326
|
+
if (definition.mode !== 'composition') {
|
|
8327
|
+
for (const [programKey, program] of programEntries) {
|
|
8328
|
+
const providers = stackProviders.get(programKey) ?? [];
|
|
8329
|
+
if (providers.length === 0)
|
|
8330
|
+
continue;
|
|
8331
|
+
const matches = providers.map((provider) => ({
|
|
8332
|
+
...provider,
|
|
8333
|
+
match: compareProgramIdentity(provider.program, program),
|
|
8334
|
+
}));
|
|
8335
|
+
const conflicting = matches.find(({ match }) => match === 'different');
|
|
8336
|
+
if (conflicting) {
|
|
8337
|
+
throw sessionProgramKeyConflict(conflicting.stackKey, programKey, conflicting.program, program);
|
|
8338
|
+
}
|
|
8339
|
+
if (matches.some(({ match }) => match === 'unproven')) {
|
|
8340
|
+
warnUnprovenSessionProgram(programKey, program, providers);
|
|
8341
|
+
continue;
|
|
8342
|
+
}
|
|
8343
|
+
const explicitRead = definition.programReads?.[programKey];
|
|
8344
|
+
if (explicitRead !== undefined) {
|
|
8345
|
+
// Sharing would read through the serving stack's configuration, so an
|
|
8346
|
+
// explicit descriptor must resolve to the same one.
|
|
8347
|
+
const servingStack = providers[0].stackKey;
|
|
8348
|
+
const standaloneRead = memberProgramRead(programAsStack(programKey, program, explicitRead), programKey, options?.programs?.[programKey], options);
|
|
8349
|
+
const stackRead = memberProgramRead(effectiveStacks.get(servingStack), programKey, options?.stacks?.[servingStack], options);
|
|
8350
|
+
if (!sameProgramRead(standaloneRead, stackRead)) {
|
|
8351
|
+
warnUnprovenSessionProgram(programKey, program, providers, 'program-read');
|
|
8352
|
+
continue;
|
|
8353
|
+
}
|
|
8354
|
+
}
|
|
8355
|
+
if (options?.programs?.[programKey] !== undefined) {
|
|
8356
|
+
throw new AreteError(`Standalone program '${programKey}' is the same program SDK that stack `
|
|
8357
|
+
+ `'${providers[0].stackKey}' provides, so the session serves it from the stack's `
|
|
8358
|
+
+ `connected instance; configure it through options.stacks.${providers[0].stackKey} `
|
|
8359
|
+
+ `instead of options.programs.${programKey}`, 'INVALID_CONFIG');
|
|
8360
|
+
}
|
|
8361
|
+
sharedStandalonePrograms.add(programKey);
|
|
8362
|
+
}
|
|
8363
|
+
}
|
|
8364
|
+
const memberProgramEntries = programEntries.filter(([key]) => !sharedStandalonePrograms.has(key));
|
|
7794
8365
|
let wallet = options?.wallet;
|
|
7795
8366
|
const signerRegistry = options?.signerRegistry ?? createSignerRegistry();
|
|
7796
8367
|
const connectedStacks = await Promise.all(stackEntries.map(async ([key, stack]) => {
|
|
7797
8368
|
const memberOptions = options?.stacks?.[key];
|
|
7798
|
-
const effectiveStack =
|
|
8369
|
+
const effectiveStack = effectiveStacks.get(key);
|
|
7799
8370
|
const connectOptions = resolveMemberConnectOptions(effectiveStack, memberOptions, {
|
|
7800
8371
|
...options,
|
|
7801
8372
|
chain: sharedChain,
|
|
@@ -7808,7 +8379,7 @@ async function createSession(definition, ...args) {
|
|
|
7808
8379
|
});
|
|
7809
8380
|
return [key, client];
|
|
7810
8381
|
}));
|
|
7811
|
-
const connectedPrograms = await Promise.all(
|
|
8382
|
+
const connectedPrograms = await Promise.all(memberProgramEntries.map(async ([key, program]) => {
|
|
7812
8383
|
const syntheticStack = programAsStack(key, program, definition.programReads?.[key]);
|
|
7813
8384
|
const connectOptions = resolveMemberConnectOptions(syntheticStack, options?.programs?.[key], {
|
|
7814
8385
|
...options,
|
|
@@ -7825,30 +8396,38 @@ async function createSession(definition, ...args) {
|
|
|
7825
8396
|
const executionHost = memberClients[0];
|
|
7826
8397
|
const transactions = sharedTransactions ?? executionHost.transactions;
|
|
7827
8398
|
const stacks = Object.fromEntries(connectedStacks);
|
|
7828
|
-
const explicitProgramKeys = new Set(connectedPrograms.map(([key]) => key));
|
|
7829
|
-
const programOwners = new Map();
|
|
7830
8399
|
const connectedProgramEntries = connectedPrograms.map(([key, client]) => [
|
|
7831
8400
|
key,
|
|
7832
8401
|
client.programs[key],
|
|
7833
8402
|
]);
|
|
7834
8403
|
const promotedPrograms = Object.fromEntries(connectedProgramEntries);
|
|
7835
8404
|
if (definition.mode !== 'composition') {
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
programOwners.set(programKey, { stack: stackKey, programId: program.programId });
|
|
8405
|
+
const connectedStackClients = new Map(connectedStacks);
|
|
8406
|
+
for (const [programKey, providers] of stackProviders) {
|
|
8407
|
+
if (Object.prototype.hasOwnProperty.call(promotedPrograms, programKey))
|
|
8408
|
+
continue;
|
|
8409
|
+
const first = providers[0];
|
|
8410
|
+
const matches = providers.flatMap((left, index) => providers.slice(index + 1).map((right) => compareProgramIdentity(left.program, right.program)));
|
|
8411
|
+
if (!matches.includes('different')) {
|
|
8412
|
+
// One program, however many stacks bundle it: the first stack's
|
|
8413
|
+
// connected instance serves the top-level key. Copies with the same
|
|
8414
|
+
// program spec that cannot be proven identical say so once.
|
|
8415
|
+
if (matches.includes('unproven'))
|
|
8416
|
+
warnUnprovenStackPrograms(programKey, providers);
|
|
8417
|
+
promotedPrograms[programKey] = connectedStackClients.get(first.stackKey).programs[programKey];
|
|
8418
|
+
continue;
|
|
7851
8419
|
}
|
|
8420
|
+
// Different program SDKs under one key: every stack keeps its own at
|
|
8421
|
+
// session.stacks.<stack>.programs.<key>, and the ambiguous top-level key
|
|
8422
|
+
// explains how to choose instead of silently picking one.
|
|
8423
|
+
const error = ambiguousSessionProgram(programKey, providers);
|
|
8424
|
+
Object.defineProperty(promotedPrograms, programKey, {
|
|
8425
|
+
get() {
|
|
8426
|
+
throw error;
|
|
8427
|
+
},
|
|
8428
|
+
enumerable: false,
|
|
8429
|
+
configurable: true,
|
|
8430
|
+
});
|
|
7852
8431
|
}
|
|
7853
8432
|
}
|
|
7854
8433
|
const programs = promotedPrograms;
|
|
@@ -8089,13 +8668,17 @@ exports.Arete = Arete;
|
|
|
8089
8668
|
exports.AreteError = AreteError;
|
|
8090
8669
|
exports.ConnectionManager = ConnectionManager;
|
|
8091
8670
|
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
|
8671
|
+
exports.DEFAULT_INITIAL_DATA_TIMEOUT_MS = DEFAULT_INITIAL_DATA_TIMEOUT_MS;
|
|
8092
8672
|
exports.DEFAULT_MAX_ENTRIES_PER_VIEW = DEFAULT_MAX_ENTRIES_PER_VIEW;
|
|
8673
|
+
exports.EXTENSION_API_VERSION = EXTENSION_API_VERSION;
|
|
8093
8674
|
exports.FrameProcessor = FrameProcessor;
|
|
8675
|
+
exports.InitialDataTimeoutError = InitialDataTimeoutError;
|
|
8094
8676
|
exports.InstructionError = InstructionError;
|
|
8095
8677
|
exports.MemoryAdapter = MemoryAdapter;
|
|
8096
8678
|
exports.OPERATION_KIND = OPERATION_KIND;
|
|
8097
8679
|
exports.OperationCallbackError = OperationCallbackError;
|
|
8098
8680
|
exports.OperationExecutionError = OperationExecutionError;
|
|
8681
|
+
exports.PROGRAM_KEY_CONFLICT = PROGRAM_KEY_CONFLICT;
|
|
8099
8682
|
exports.PROGRAM_OPERATION_EXTENSIONS = PROGRAM_OPERATION_EXTENSIONS;
|
|
8100
8683
|
exports.ProcessedSlotTimeoutError = ProcessedSlotTimeoutError;
|
|
8101
8684
|
exports.QueryStore = QueryStore;
|
|
@@ -8119,6 +8702,7 @@ exports.buildInstruction = buildInstruction;
|
|
|
8119
8702
|
exports.bytes = bytes;
|
|
8120
8703
|
exports.canonicalQueryKey = canonicalQueryKey;
|
|
8121
8704
|
exports.chainAccountLoader = chainAccountLoader;
|
|
8705
|
+
exports.compareProgramIdentity = compareProgramIdentity;
|
|
8122
8706
|
exports.createChainClient = createChainClient;
|
|
8123
8707
|
exports.createEntityStream = createEntityStream;
|
|
8124
8708
|
exports.createHostedSolanaGatewayTransports = createHostedSolanaGatewayTransports;
|
|
@@ -8171,6 +8755,7 @@ exports.isEntityFrame = isEntityFrame;
|
|
|
8171
8755
|
exports.isErrorFrame = isErrorFrame;
|
|
8172
8756
|
exports.isHostedAreteEndpoint = isHostedAreteEndpoint;
|
|
8173
8757
|
exports.isReplayErrorCode = isReplayErrorCode;
|
|
8758
|
+
exports.isSameProgramSdk = isSameProgramSdk;
|
|
8174
8759
|
exports.isSnapshotFrame = isSnapshotFrame;
|
|
8175
8760
|
exports.isStackVersionRefusalCode = isStackVersionRefusalCode;
|
|
8176
8761
|
exports.isSubscribedFrame = isSubscribedFrame;
|
|
@@ -8210,6 +8795,7 @@ exports.unwrapOperationExecutionError = unwrapOperationExecutionError;
|
|
|
8210
8795
|
exports.validateAccountResolution = validateAccountResolution;
|
|
8211
8796
|
exports.validateProgramReadDescriptor = validateProgramReadDescriptor;
|
|
8212
8797
|
exports.validateSubscriptionId = validateSubscriptionId;
|
|
8798
|
+
exports.withProgramIdentity = withProgramIdentity;
|
|
8213
8799
|
exports.withProgramRead = withProgramRead;
|
|
8214
8800
|
exports.withPrograms = withPrograms;
|
|
8215
8801
|
//# sourceMappingURL=index.cjs.map
|