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