@usearete/sdk 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -1
- package/dist/index.cjs +577 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +196 -8
- package/dist/index.esm.js +573 -108
- 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
|
@@ -1874,6 +1874,7 @@ class ConnectionManager {
|
|
|
1874
1874
|
}
|
|
1875
1875
|
|
|
1876
1876
|
const INTERNAL_SEQ_FIELD = '__seq';
|
|
1877
|
+
const INTERNAL_VERSION_FIELD = '__version';
|
|
1877
1878
|
class ProcessedSlotTimeoutError extends Error {
|
|
1878
1879
|
constructor(targetSlot, processedSlot) {
|
|
1879
1880
|
super(`Timed out waiting for Arete to process slot ${targetSlot}`);
|
|
@@ -1934,6 +1935,9 @@ function toCamelCaseSegment(value) {
|
|
|
1934
1935
|
if (value === '_seq') {
|
|
1935
1936
|
return INTERNAL_SEQ_FIELD;
|
|
1936
1937
|
}
|
|
1938
|
+
if (value === '_version') {
|
|
1939
|
+
return INTERNAL_VERSION_FIELD;
|
|
1940
|
+
}
|
|
1937
1941
|
const pascal = value
|
|
1938
1942
|
.split(/[_.-]/)
|
|
1939
1943
|
.filter(segment => segment.length > 0)
|
|
@@ -1944,6 +1948,16 @@ function toCamelCaseSegment(value) {
|
|
|
1944
1948
|
}
|
|
1945
1949
|
return pascal[0].toLowerCase() + pascal.slice(1);
|
|
1946
1950
|
}
|
|
1951
|
+
/** `{epoch}:{counter}` from a frame's `_version`, or null if it does not parse. */
|
|
1952
|
+
function parseVersion(version) {
|
|
1953
|
+
const separator = version.lastIndexOf(':');
|
|
1954
|
+
if (separator <= 0)
|
|
1955
|
+
return null;
|
|
1956
|
+
const counter = version.slice(separator + 1);
|
|
1957
|
+
if (!/^\d+$/.test(counter))
|
|
1958
|
+
return null;
|
|
1959
|
+
return { epoch: version.slice(0, separator), counter: BigInt(counter) };
|
|
1960
|
+
}
|
|
1947
1961
|
class FrameProcessor {
|
|
1948
1962
|
/**
|
|
1949
1963
|
* Observe the connection's cursor tracker instead of a private one.
|
|
@@ -1962,6 +1976,15 @@ class FrameProcessor {
|
|
|
1962
1976
|
this.latestProcessedSlot = null;
|
|
1963
1977
|
this.processedSlotWaiters = new Set();
|
|
1964
1978
|
this.cursors = new CursorTracker();
|
|
1979
|
+
/**
|
|
1980
|
+
* Whether the server's latest acknowledgement guaranteed whole entities.
|
|
1981
|
+
* One connection talks to one server, so its latest ack speaks for every
|
|
1982
|
+
* subscription on it; a reconnect re-acknowledges each one.
|
|
1983
|
+
*/
|
|
1984
|
+
this.serverSendsWholeEntities = false;
|
|
1985
|
+
/** Keys dropped by `maxEntriesPerView`, per view, oldest first; bounded. */
|
|
1986
|
+
this.evictedKeys = new Map();
|
|
1987
|
+
this.warnedEvictedViews = new Set();
|
|
1965
1988
|
this.storage = storage;
|
|
1966
1989
|
this.maxEntriesPerView = config.maxEntriesPerView === undefined
|
|
1967
1990
|
? DEFAULT_MAX_ENTRIES_PER_VIEW
|
|
@@ -2009,18 +2032,22 @@ class FrameProcessor {
|
|
|
2009
2032
|
const diagnostic = {
|
|
2010
2033
|
view: viewPath,
|
|
2011
2034
|
...context,
|
|
2035
|
+
reason: 'schema',
|
|
2012
2036
|
error: validationError,
|
|
2013
2037
|
};
|
|
2038
|
+
this.reportDiagnostic(diagnostic);
|
|
2039
|
+
if (this.warnOnValidationError) {
|
|
2040
|
+
console.warn('[Arete] Frame validation failed:', diagnostic);
|
|
2041
|
+
}
|
|
2042
|
+
return null;
|
|
2043
|
+
}
|
|
2044
|
+
reportDiagnostic(diagnostic) {
|
|
2014
2045
|
try {
|
|
2015
2046
|
this.onValidationError?.(diagnostic);
|
|
2016
2047
|
}
|
|
2017
2048
|
catch (callbackError) {
|
|
2018
2049
|
console.error('[Arete] Frame validation callback failed:', callbackError);
|
|
2019
2050
|
}
|
|
2020
|
-
if (this.warnOnValidationError) {
|
|
2021
|
-
console.warn('[Arete] Frame validation failed:', diagnostic);
|
|
2022
|
-
}
|
|
2023
|
-
return null;
|
|
2024
2051
|
}
|
|
2025
2052
|
hasSchema(viewPath) {
|
|
2026
2053
|
return this.getSchema(viewPath) !== null;
|
|
@@ -2087,6 +2114,50 @@ class FrameProcessor {
|
|
|
2087
2114
|
}
|
|
2088
2115
|
return leftIndex.localeCompare(rightIndex);
|
|
2089
2116
|
}
|
|
2117
|
+
extractVersion(data) {
|
|
2118
|
+
if (!isObject(data)) {
|
|
2119
|
+
return undefined;
|
|
2120
|
+
}
|
|
2121
|
+
const version = data._version;
|
|
2122
|
+
return typeof version === 'string' ? version : undefined;
|
|
2123
|
+
}
|
|
2124
|
+
getInternalVersion(data) {
|
|
2125
|
+
if (!isObject(data)) {
|
|
2126
|
+
return undefined;
|
|
2127
|
+
}
|
|
2128
|
+
const version = data[INTERNAL_VERSION_FIELD];
|
|
2129
|
+
return typeof version === 'string' ? version : undefined;
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Whether a frame at `incoming` is one this client already applied, or older.
|
|
2133
|
+
* A version is `{epoch}:{counter}`, and counters compare only within an
|
|
2134
|
+
* epoch: a server restart or a reloaded stack starts a new one, whose first
|
|
2135
|
+
* frame is newer than anything from the old one. A version that does not
|
|
2136
|
+
* parse cannot be ordered and is never stale.
|
|
2137
|
+
*/
|
|
2138
|
+
isStaleVersion(incoming, held) {
|
|
2139
|
+
if (held === undefined) {
|
|
2140
|
+
return false;
|
|
2141
|
+
}
|
|
2142
|
+
const next = parseVersion(incoming);
|
|
2143
|
+
const current = parseVersion(held);
|
|
2144
|
+
if (next === null || current === null || next.epoch !== current.epoch) {
|
|
2145
|
+
return false;
|
|
2146
|
+
}
|
|
2147
|
+
return next.counter <= current.counter;
|
|
2148
|
+
}
|
|
2149
|
+
attachInternalVersion(data, version) {
|
|
2150
|
+
if (!version || !isObject(data)) {
|
|
2151
|
+
return data;
|
|
2152
|
+
}
|
|
2153
|
+
Object.defineProperty(data, INTERNAL_VERSION_FIELD, {
|
|
2154
|
+
value: version,
|
|
2155
|
+
enumerable: false,
|
|
2156
|
+
configurable: true,
|
|
2157
|
+
writable: true,
|
|
2158
|
+
});
|
|
2159
|
+
return data;
|
|
2160
|
+
}
|
|
2090
2161
|
attachInternalSeq(_viewPath, data, seq) {
|
|
2091
2162
|
if (!seq || !isObject(data)) {
|
|
2092
2163
|
return data;
|
|
@@ -2307,6 +2378,7 @@ class FrameProcessor {
|
|
|
2307
2378
|
}
|
|
2308
2379
|
handleSubscribedFrame(frame) {
|
|
2309
2380
|
this.cursors.observe(frame);
|
|
2381
|
+
this.serverSendsWholeEntities = frame.wholeEntities === true;
|
|
2310
2382
|
const viewPath = frame.query.view;
|
|
2311
2383
|
if (this.storage.setViewConfig && frame.sort) {
|
|
2312
2384
|
this.storage.setViewConfig(viewPath, {
|
|
@@ -2331,10 +2403,11 @@ class FrameProcessor {
|
|
|
2331
2403
|
if (normalized === null) {
|
|
2332
2404
|
continue;
|
|
2333
2405
|
}
|
|
2334
|
-
const nextValue = this.attachInternalSeq(viewPath, normalized, this.extractSeq(entity.data));
|
|
2406
|
+
const nextValue = this.attachInternalVersion(this.attachInternalSeq(viewPath, normalized, this.extractSeq(entity.data)), this.extractVersion(entity.data));
|
|
2335
2407
|
acceptedKeys.push(entity.key);
|
|
2336
2408
|
const previousValue = this.storage.get(viewPath, entity.key);
|
|
2337
2409
|
this.storage.set(viewPath, entity.key, nextValue);
|
|
2410
|
+
this.forgetEvicted(viewPath, entity.key);
|
|
2338
2411
|
this.storage.notifyUpdate(viewPath, entity.key, {
|
|
2339
2412
|
type: 'upsert',
|
|
2340
2413
|
key: entity.key,
|
|
@@ -2353,18 +2426,24 @@ class FrameProcessor {
|
|
|
2353
2426
|
const previousValue = this.storage.get(viewPath, frame.key);
|
|
2354
2427
|
const previousSequence = this.getInternalSeq(previousValue);
|
|
2355
2428
|
const cursor = this.cursors.observe(frame);
|
|
2356
|
-
//
|
|
2357
|
-
//
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2429
|
+
// A server that stamps `_version` orders one key's frames by it. `seq`
|
|
2430
|
+
// cannot: every update decoded from one transaction shares one, and within
|
|
2431
|
+
// a slot account updates and instructions number themselves differently,
|
|
2432
|
+
// so a later frame can carry a lower `seq`. Without a version (an older
|
|
2433
|
+
// server) the `seq` rule applies. On a tape the offset is the identity, so
|
|
2434
|
+
// neither does.
|
|
2435
|
+
const frameVersion = this.extractVersion(frame.data);
|
|
2436
|
+
const duplicateOrStale = frame.offset === undefined && (frameVersion !== undefined
|
|
2437
|
+
? this.isStaleVersion(frameVersion, this.getInternalVersion(previousValue))
|
|
2438
|
+
: frame.seq !== undefined
|
|
2439
|
+
&& previousSequence !== undefined
|
|
2440
|
+
&& this.compareSeq(frame.seq, previousSequence) <= 0);
|
|
2362
2441
|
switch (frame.op) {
|
|
2363
2442
|
case 'upsert':
|
|
2364
2443
|
{
|
|
2365
2444
|
if (frame.data === null)
|
|
2366
2445
|
break;
|
|
2367
|
-
if (
|
|
2446
|
+
if (duplicateOrStale && previousValue !== null) {
|
|
2368
2447
|
const update = {
|
|
2369
2448
|
type: 'upsert',
|
|
2370
2449
|
key: frame.key,
|
|
@@ -2381,14 +2460,17 @@ class FrameProcessor {
|
|
|
2381
2460
|
if (normalized === null) {
|
|
2382
2461
|
break;
|
|
2383
2462
|
}
|
|
2384
|
-
const nextValue = this.attachInternalSeq(viewPath, normalized,
|
|
2463
|
+
const nextValue = this.attachInternalVersion(this.attachInternalSeq(viewPath, normalized,
|
|
2385
2464
|
// Fall back to the previous entity's sequence, as the patch branch
|
|
2386
2465
|
// below does. Without this an unsequenced upsert drops the tracked
|
|
2387
2466
|
// sequence (it rides on the object being replaced), disarming the
|
|
2388
2467
|
// staleness guard until the next sequenced frame and letting a
|
|
2389
2468
|
// later older frame overwrite newer data.
|
|
2390
|
-
frame.seq ?? this.extractSeq(frame.data) ?? this.getInternalSeq(previousValue))
|
|
2469
|
+
frame.seq ?? this.extractSeq(frame.data) ?? this.getInternalSeq(previousValue)),
|
|
2470
|
+
// An unversioned write keeps the version it replaces, likewise.
|
|
2471
|
+
frameVersion ?? this.getInternalVersion(previousValue));
|
|
2391
2472
|
this.storage.set(viewPath, frame.key, nextValue);
|
|
2473
|
+
this.forgetEvicted(viewPath, frame.key);
|
|
2392
2474
|
const update = {
|
|
2393
2475
|
type: 'upsert',
|
|
2394
2476
|
key: frame.key,
|
|
@@ -2405,6 +2487,19 @@ class FrameProcessor {
|
|
|
2405
2487
|
if (frame.data === null)
|
|
2406
2488
|
break;
|
|
2407
2489
|
const existing = this.storage.get(viewPath, frame.key);
|
|
2490
|
+
if (existing === null && frame.offset === undefined && this.serverSendsWholeEntities) {
|
|
2491
|
+
// A patch carries only changed fields. With no entity to merge into
|
|
2492
|
+
// (never received, or evicted) it would be stored as a partial
|
|
2493
|
+
// entity typed as complete, so it is discarded: no storage write, no
|
|
2494
|
+
// sequence, no query membership, no update. The key appears when the
|
|
2495
|
+
// server sends a full upsert, which a server acknowledging
|
|
2496
|
+
// `wholeEntities` guarantees for every key it has not sent. Tape
|
|
2497
|
+
// records (frames with an `offset`) are events rather than entity
|
|
2498
|
+
// state and are always applied: a consumer resuming from a cursor
|
|
2499
|
+
// holds what came before it.
|
|
2500
|
+
this.reportDiscardedPatch(viewPath, frame);
|
|
2501
|
+
break;
|
|
2502
|
+
}
|
|
2408
2503
|
const normalizedPatch = this.normalizeEntity(viewPath, frame.data, {
|
|
2409
2504
|
key: frame.key,
|
|
2410
2505
|
operation: 'patch',
|
|
@@ -2413,7 +2508,7 @@ class FrameProcessor {
|
|
|
2413
2508
|
if (normalizedPatch === null) {
|
|
2414
2509
|
break;
|
|
2415
2510
|
}
|
|
2416
|
-
if (
|
|
2511
|
+
if (duplicateOrStale && existing !== null) {
|
|
2417
2512
|
const update = {
|
|
2418
2513
|
type: 'patch',
|
|
2419
2514
|
key: frame.key,
|
|
@@ -2426,7 +2521,7 @@ class FrameProcessor {
|
|
|
2426
2521
|
const merged = existing
|
|
2427
2522
|
? deepMergeWithAppend(existing, normalizedPatch, appendPaths)
|
|
2428
2523
|
: normalizedPatch;
|
|
2429
|
-
const nextValue = this.attachInternalSeq(viewPath, merged, frame.seq ?? this.extractSeq(frame.data) ?? this.getInternalSeq(existing));
|
|
2524
|
+
const nextValue = this.attachInternalVersion(this.attachInternalSeq(viewPath, merged, frame.seq ?? this.extractSeq(frame.data) ?? this.getInternalSeq(existing)), frameVersion ?? this.getInternalVersion(existing));
|
|
2430
2525
|
this.storage.set(viewPath, frame.key, nextValue);
|
|
2431
2526
|
const update = {
|
|
2432
2527
|
type: 'patch',
|
|
@@ -2445,6 +2540,7 @@ class FrameProcessor {
|
|
|
2445
2540
|
break;
|
|
2446
2541
|
case 'delete':
|
|
2447
2542
|
this.storage.delete(viewPath, frame.key);
|
|
2543
|
+
this.forgetEvicted(viewPath, frame.key);
|
|
2448
2544
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
2449
2545
|
type: 'delete',
|
|
2450
2546
|
key: frame.key,
|
|
@@ -2481,8 +2577,56 @@ class FrameProcessor {
|
|
|
2481
2577
|
return;
|
|
2482
2578
|
while (this.storage.size(viewPath) > this.maxEntriesPerView) {
|
|
2483
2579
|
const evicted = this.storage.evictOldest(viewPath);
|
|
2484
|
-
if (evicted
|
|
2485
|
-
|
|
2580
|
+
if (evicted === undefined)
|
|
2581
|
+
continue;
|
|
2582
|
+
this.queryStore?.evict(viewPath, evicted);
|
|
2583
|
+
this.rememberEvicted(viewPath, evicted, this.maxEntriesPerView);
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
/**
|
|
2587
|
+
* Remember a key `maxEntriesPerView` dropped, so a later patch for it can be
|
|
2588
|
+
* reported as such. At most `limit` keys per view, most recent kept.
|
|
2589
|
+
*/
|
|
2590
|
+
rememberEvicted(viewPath, key, limit) {
|
|
2591
|
+
let keys = this.evictedKeys.get(viewPath);
|
|
2592
|
+
if (!keys) {
|
|
2593
|
+
keys = new Set();
|
|
2594
|
+
this.evictedKeys.set(viewPath, keys);
|
|
2595
|
+
}
|
|
2596
|
+
keys.delete(key);
|
|
2597
|
+
keys.add(key);
|
|
2598
|
+
while (keys.size > Math.max(limit, 1)) {
|
|
2599
|
+
const oldest = keys.values().next().value;
|
|
2600
|
+
if (oldest === undefined)
|
|
2601
|
+
break;
|
|
2602
|
+
keys.delete(oldest);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
forgetEvicted(viewPath, key) {
|
|
2606
|
+
this.evictedKeys.get(viewPath)?.delete(key);
|
|
2607
|
+
}
|
|
2608
|
+
reportDiscardedPatch(viewPath, frame) {
|
|
2609
|
+
const seq = frame.seq ?? this.extractSeq(frame.data);
|
|
2610
|
+
const evicted = this.evictedKeys.get(viewPath)?.has(frame.key) === true;
|
|
2611
|
+
const error = evicted
|
|
2612
|
+
? new AreteError(`Discarded patch for '${frame.key}' in ${viewPath}: this client dropped the entity to `
|
|
2613
|
+
+ `stay within maxEntriesPerView (${String(this.maxEntriesPerView)}), and the server `
|
|
2614
|
+
+ 'sends no upsert for it until it re-enters the query or the subscription is '
|
|
2615
|
+
+ 're-established; raise maxEntriesPerView above the subscription size', 'PATCH_FOR_EVICTED_KEY')
|
|
2616
|
+
: new AreteError(`Discarded patch for '${frame.key}' in ${viewPath}: the client holds no entity for `
|
|
2617
|
+
+ 'this key; it appears when the server sends a full upsert', 'PATCH_FOR_UNKNOWN_KEY');
|
|
2618
|
+
const diagnostic = {
|
|
2619
|
+
view: viewPath,
|
|
2620
|
+
key: frame.key,
|
|
2621
|
+
...(seq !== undefined ? { seq } : {}),
|
|
2622
|
+
operation: 'patch',
|
|
2623
|
+
reason: evicted ? 'evicted-key' : 'unknown-key',
|
|
2624
|
+
error,
|
|
2625
|
+
};
|
|
2626
|
+
this.reportDiagnostic(diagnostic);
|
|
2627
|
+
if (evicted && this.warnOnValidationError && !this.warnedEvictedViews.has(viewPath)) {
|
|
2628
|
+
this.warnedEvictedViews.add(viewPath);
|
|
2629
|
+
console.warn('[Arete] Patch dropped for a locally evicted entity:', diagnostic);
|
|
2486
2630
|
}
|
|
2487
2631
|
}
|
|
2488
2632
|
}
|
|
@@ -2629,6 +2773,66 @@ class MemoryAdapter {
|
|
|
2629
2773
|
}
|
|
2630
2774
|
}
|
|
2631
2775
|
|
|
2776
|
+
/** Whether a sort value cannot be ranked: it is missing or `null`. */
|
|
2777
|
+
function isUnrankedSortValue(value) {
|
|
2778
|
+
return value === undefined || value === null;
|
|
2779
|
+
}
|
|
2780
|
+
/**
|
|
2781
|
+
* Compares two ranked sort values in ascending order. Numbers and bigints
|
|
2782
|
+
* compare numerically (also against each other), booleans as `false < true`,
|
|
2783
|
+
* and everything else with `localeCompare` on its string form (the SDK
|
|
2784
|
+
* ordering rule, shared with the Rust and Python SDKs).
|
|
2785
|
+
*
|
|
2786
|
+
* This is not the server's ranking. The server compares strings by byte
|
|
2787
|
+
* order, except that two decimal-integer strings compare numerically, and it
|
|
2788
|
+
* ranks numbers below strings and booleans below both. The server's order
|
|
2789
|
+
* decides which entities fill a `take` window; this one only orders the
|
|
2790
|
+
* members the client already holds, so the two can list a window's members
|
|
2791
|
+
* differently, never choose different members.
|
|
2792
|
+
*/
|
|
2793
|
+
function compareSortValues(left, right) {
|
|
2794
|
+
if (left === right)
|
|
2795
|
+
return 0;
|
|
2796
|
+
if (isUnrankedSortValue(left))
|
|
2797
|
+
return isUnrankedSortValue(right) ? 0 : 1;
|
|
2798
|
+
if (isUnrankedSortValue(right))
|
|
2799
|
+
return -1;
|
|
2800
|
+
if (typeof left === 'number' && typeof right === 'number')
|
|
2801
|
+
return left - right;
|
|
2802
|
+
if (typeof left === 'bigint' && typeof right === 'bigint')
|
|
2803
|
+
return left < right ? -1 : 1;
|
|
2804
|
+
if (typeof left === 'bigint' && typeof right === 'number' && Number.isInteger(right)) {
|
|
2805
|
+
const other = BigInt(right);
|
|
2806
|
+
return left === other ? 0 : left < other ? -1 : 1;
|
|
2807
|
+
}
|
|
2808
|
+
if (typeof left === 'number' && typeof right === 'bigint' && Number.isInteger(left)) {
|
|
2809
|
+
const other = BigInt(left);
|
|
2810
|
+
return other === right ? 0 : other < right ? -1 : 1;
|
|
2811
|
+
}
|
|
2812
|
+
if (typeof left === 'boolean' && typeof right === 'boolean')
|
|
2813
|
+
return Number(left) - Number(right);
|
|
2814
|
+
return String(left).localeCompare(String(right));
|
|
2815
|
+
}
|
|
2816
|
+
/**
|
|
2817
|
+
* Orders two entities' sort values for a view sorted in `order`.
|
|
2818
|
+
*
|
|
2819
|
+
* A missing or `null` sort value cannot be ranked, so it sorts after every
|
|
2820
|
+
* ranked value in both directions: `desc` reverses only the comparison between
|
|
2821
|
+
* two ranked values. Two unranked values tie. Callers break ties on the entity
|
|
2822
|
+
* key, ascending, whatever the order. The unranked-last rule is the server's
|
|
2823
|
+
* and the Rust and Python SDKs'; how two ranked values compare is
|
|
2824
|
+
* `compareRanked` (by default {@link compareSortValues}, which differs from
|
|
2825
|
+
* the server as described there).
|
|
2826
|
+
*/
|
|
2827
|
+
function compareSortOrder(left, right, order, compareRanked = compareSortValues) {
|
|
2828
|
+
const leftUnranked = isUnrankedSortValue(left);
|
|
2829
|
+
const rightUnranked = isUnrankedSortValue(right);
|
|
2830
|
+
if (leftUnranked || rightUnranked)
|
|
2831
|
+
return Number(leftUnranked) - Number(rightUnranked);
|
|
2832
|
+
const compared = compareRanked(left, right);
|
|
2833
|
+
return order === 'desc' ? -compared : compared;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2632
2836
|
function getNestedValue$1(obj, path) {
|
|
2633
2837
|
let current = obj;
|
|
2634
2838
|
for (const segment of path) {
|
|
@@ -2640,33 +2844,6 @@ function getNestedValue$1(obj, path) {
|
|
|
2640
2844
|
}
|
|
2641
2845
|
return current;
|
|
2642
2846
|
}
|
|
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
2847
|
class SortedStorageDecorator {
|
|
2671
2848
|
constructor(inner) {
|
|
2672
2849
|
this.sortConfigs = new Map();
|
|
@@ -2786,7 +2963,6 @@ class SortedStorageDecorator {
|
|
|
2786
2963
|
}
|
|
2787
2964
|
binarySearchInsertPosition(viewPath, sortedKeys, sortConfig, newKey, newValue) {
|
|
2788
2965
|
const newSortValue = getNestedValue$1(newValue, sortConfig.field);
|
|
2789
|
-
const isDesc = sortConfig.order === 'desc';
|
|
2790
2966
|
let low = 0;
|
|
2791
2967
|
let high = sortedKeys.length;
|
|
2792
2968
|
while (low < high) {
|
|
@@ -2794,9 +2970,8 @@ class SortedStorageDecorator {
|
|
|
2794
2970
|
const midKey = sortedKeys[mid];
|
|
2795
2971
|
const midEntity = this.inner.get(viewPath, midKey);
|
|
2796
2972
|
const midValue = getNestedValue$1(midEntity, sortConfig.field);
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
cmp = -cmp;
|
|
2973
|
+
// Unranked (missing or null) values sort last in both directions.
|
|
2974
|
+
let cmp = compareSortOrder(newSortValue, midValue, sortConfig.order);
|
|
2800
2975
|
if (cmp === 0) {
|
|
2801
2976
|
cmp = newKey.localeCompare(midKey);
|
|
2802
2977
|
}
|
|
@@ -2813,14 +2988,11 @@ class SortedStorageDecorator {
|
|
|
2813
2988
|
const allKeys = this.inner.keys(viewPath);
|
|
2814
2989
|
if (allKeys.length === 0)
|
|
2815
2990
|
return;
|
|
2816
|
-
const isDesc = sortConfig.order === 'desc';
|
|
2817
2991
|
const entries = allKeys.map(k => [k, this.inner.get(viewPath, k)]);
|
|
2818
2992
|
entries.sort((a, b) => {
|
|
2819
2993
|
const aValue = getNestedValue$1(a[1], sortConfig.field);
|
|
2820
2994
|
const bValue = getNestedValue$1(b[1], sortConfig.field);
|
|
2821
|
-
let cmp =
|
|
2822
|
-
if (isDesc)
|
|
2823
|
-
cmp = -cmp;
|
|
2995
|
+
let cmp = compareSortOrder(aValue, bValue, sortConfig.order);
|
|
2824
2996
|
if (cmp === 0) {
|
|
2825
2997
|
cmp = a[0].localeCompare(b[0]);
|
|
2826
2998
|
}
|
|
@@ -2839,25 +3011,9 @@ function getNestedValue(value, path) {
|
|
|
2839
3011
|
}
|
|
2840
3012
|
return current;
|
|
2841
3013
|
}
|
|
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
3014
|
function compareSequences(left, right) {
|
|
2859
3015
|
if (typeof left !== 'string' || typeof right !== 'string')
|
|
2860
|
-
return
|
|
3016
|
+
return compareSortValues(left, right);
|
|
2861
3017
|
const [leftSlot, leftIndex = ''] = left.split(':', 2);
|
|
2862
3018
|
const [rightSlot, rightIndex = ''] = right.split(':', 2);
|
|
2863
3019
|
if (/^\d+$/.test(leftSlot ?? '') && /^\d+$/.test(rightSlot ?? '')) {
|
|
@@ -3149,9 +3305,9 @@ class QueryStore {
|
|
|
3149
3305
|
const right = field
|
|
3150
3306
|
? getNestedValue(this.storage.get(view, rightKey), field)
|
|
3151
3307
|
: record.sequences.get(rightKey);
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3308
|
+
// A missing or null sort value (or sequence) sorts after every ranked
|
|
3309
|
+
// one in both directions; ties break on the key, always ascending.
|
|
3310
|
+
const compared = compareSortOrder(left, right, order, field ? compareSortValues : compareSequences);
|
|
3155
3311
|
return compared === 0 ? leftKey.localeCompare(rightKey) : compared;
|
|
3156
3312
|
});
|
|
3157
3313
|
}
|
|
@@ -6227,16 +6383,22 @@ const PROGRAM_READ_DESCRIPTOR = Symbol.for('@usearete/sdk/program-read-descripto
|
|
|
6227
6383
|
/**
|
|
6228
6384
|
* Bundle a generated program definition with its default read descriptor.
|
|
6229
6385
|
*
|
|
6230
|
-
* Generated SDK entries call this automatically. The descriptor
|
|
6231
|
-
*
|
|
6386
|
+
* Generated SDK entries call this automatically. The descriptor lives under a
|
|
6387
|
+
* registry symbol, so `Object.keys` and `JSON.stringify` never see it, while
|
|
6388
|
+
* `{ ...program }` keeps it: a spread program still reads through its release.
|
|
6389
|
+
* Every other own property (including the runtime extension carrier) is copied
|
|
6390
|
+
* unchanged, except `packageReleaseHash`: the result reads differently from
|
|
6391
|
+
* the generated program SDK, so it is no longer provably that SDK. Generated
|
|
6392
|
+
* entries apply `withProgramIdentity` last.
|
|
6232
6393
|
*/
|
|
6233
6394
|
function withProgramRead(program, descriptor) {
|
|
6234
6395
|
const properties = Object.getOwnPropertyDescriptors(program);
|
|
6235
6396
|
Reflect.deleteProperty(properties, PROGRAM_READ_DESCRIPTOR);
|
|
6397
|
+
Reflect.deleteProperty(properties, 'packageReleaseHash');
|
|
6236
6398
|
const bundled = Object.create(Object.getPrototypeOf(program), properties);
|
|
6237
6399
|
Object.defineProperty(bundled, PROGRAM_READ_DESCRIPTOR, {
|
|
6238
6400
|
value: descriptor,
|
|
6239
|
-
enumerable:
|
|
6401
|
+
enumerable: true,
|
|
6240
6402
|
configurable: false,
|
|
6241
6403
|
writable: false,
|
|
6242
6404
|
});
|
|
@@ -6246,9 +6408,62 @@ function withProgramRead(program, descriptor) {
|
|
|
6246
6408
|
function getProgramReadDescriptor(program) {
|
|
6247
6409
|
return program?.[PROGRAM_READ_DESCRIPTOR];
|
|
6248
6410
|
}
|
|
6411
|
+
/**
|
|
6412
|
+
* Stamp a program definition with the identity of the program SDK it is.
|
|
6413
|
+
*
|
|
6414
|
+
* Generated program SDK entries call this last, after the package's own
|
|
6415
|
+
* extension and read descriptor are applied, so the identity describes
|
|
6416
|
+
* exactly the generated SDK. `extendProgram`, `extendPrograms` and
|
|
6417
|
+
* `withProgramRead` drop the identity again: a program changed outside its
|
|
6418
|
+
* generated SDK is no longer provably that SDK. Every other own property,
|
|
6419
|
+
* including symbol-keyed runtime extensions and the read descriptor, is copied
|
|
6420
|
+
* unchanged. An absent or empty `packageReleaseHash` removes the identity.
|
|
6421
|
+
*/
|
|
6422
|
+
function withProgramIdentity(program, identity) {
|
|
6423
|
+
const properties = Object.getOwnPropertyDescriptors(program);
|
|
6424
|
+
Reflect.deleteProperty(properties, 'packageReleaseHash');
|
|
6425
|
+
const stamped = Object.create(Object.getPrototypeOf(program), properties);
|
|
6426
|
+
if (typeof identity.packageReleaseHash === 'string' && identity.packageReleaseHash.length > 0) {
|
|
6427
|
+
Object.defineProperty(stamped, 'packageReleaseHash', {
|
|
6428
|
+
value: identity.packageReleaseHash,
|
|
6429
|
+
enumerable: true,
|
|
6430
|
+
configurable: true,
|
|
6431
|
+
writable: true,
|
|
6432
|
+
});
|
|
6433
|
+
}
|
|
6434
|
+
return stamped;
|
|
6435
|
+
}
|
|
6249
6436
|
|
|
6250
|
-
|
|
6251
|
-
|
|
6437
|
+
/**
|
|
6438
|
+
* Version of the extension contract: `defineStackExtensions`,
|
|
6439
|
+
* `defineProgramExtensions`, `extendProgram`, `extendPrograms`, `extendStack`,
|
|
6440
|
+
* `withProgramRead`, and the instruction helpers generated code imports
|
|
6441
|
+
* (`createInstructionHandler`, `buildInstruction`, `instructionOperation`,
|
|
6442
|
+
* `createPreparedInstruction`, the PDA and read helpers, …).
|
|
6443
|
+
*
|
|
6444
|
+
* It only changes on a breaking change to that contract, so generated SDKs and
|
|
6445
|
+
* extensions built for one value keep working with every SDK release that
|
|
6446
|
+
* exports the same value. The same number is published in this package's
|
|
6447
|
+
* `package.json` as `arete.extensionApi` for tools that read `node_modules`.
|
|
6448
|
+
*/
|
|
6449
|
+
const EXTENSION_API_VERSION = 1;
|
|
6450
|
+
/**
|
|
6451
|
+
* Runtime extension carriers live under registry symbols (`Symbol.for`), so
|
|
6452
|
+
* two copies of this module still read each other's definitions. The
|
|
6453
|
+
* properties are enumerable: `{ ...stack }` and `{ ...program }` keep them,
|
|
6454
|
+
* while `Object.keys`, `for…in` and `JSON.stringify` never see a symbol key.
|
|
6455
|
+
*
|
|
6456
|
+
* The constants are typed as their historical property names rather than as
|
|
6457
|
+
* `unique symbol`. Generated program SDKs use them as computed keys inside
|
|
6458
|
+
* object literals, and TypeScript can only name a `unique symbol` key in
|
|
6459
|
+
* emitted declarations from a file that imports it; a string-literal key type
|
|
6460
|
+
* keeps every generated `.d.ts` nameable and identical across module copies.
|
|
6461
|
+
* Always read and write these properties through the constants (or
|
|
6462
|
+
* `getStackRuntimeExtensions` / `getProgramRuntimeExtensions`), never by the
|
|
6463
|
+
* literal name.
|
|
6464
|
+
*/
|
|
6465
|
+
const STACK_RUNTIME_EXTENSIONS = Symbol.for('@usearete/sdk/stack-runtime-extensions');
|
|
6466
|
+
const PROGRAM_OPERATION_EXTENSIONS = Symbol.for('@usearete/sdk/program-runtime-extensions');
|
|
6252
6467
|
function defineProgramExtensions() {
|
|
6253
6468
|
return (extensions) => extensions;
|
|
6254
6469
|
}
|
|
@@ -6277,7 +6492,12 @@ function extendProgram(program, extensions) {
|
|
|
6277
6492
|
if (readDescriptor) {
|
|
6278
6493
|
Object.defineProperty(extended, PROGRAM_READ_DESCRIPTOR, readDescriptor);
|
|
6279
6494
|
}
|
|
6495
|
+
// The extended program is no longer byte-for-byte the generated one, nor
|
|
6496
|
+
// provably the program SDK its package release describes: extensions
|
|
6497
|
+
// applied outside generated code must not borrow that identity. Generated
|
|
6498
|
+
// entries stamp it back with `withProgramIdentity` after their own.
|
|
6280
6499
|
delete extended.sdkDefinitionHash;
|
|
6500
|
+
delete extended.packageReleaseHash;
|
|
6281
6501
|
for (const key of ['pdas', 'accounts', 'queries', 'addresses', 'constants', 'defaults', 'math']) {
|
|
6282
6502
|
const extensionValue = extensions[key];
|
|
6283
6503
|
if (extensionValue !== undefined) {
|
|
@@ -6313,7 +6533,8 @@ function extendProgram(program, extensions) {
|
|
|
6313
6533
|
return mergeNamespace(baseRead, extensionReadFactory?.(context));
|
|
6314
6534
|
},
|
|
6315
6535
|
},
|
|
6316
|
-
|
|
6536
|
+
// Enumerable so spreading the extended program keeps its operations.
|
|
6537
|
+
enumerable: true,
|
|
6317
6538
|
configurable: false,
|
|
6318
6539
|
writable: false,
|
|
6319
6540
|
});
|
|
@@ -6358,7 +6579,8 @@ function extendStack(stack, extensions) {
|
|
|
6358
6579
|
? (client) => mergeNamespace(baseRuntime.createFlows(client), extensions.createFlows(client))
|
|
6359
6580
|
: extensions.createFlows ?? baseRuntime?.createFlows,
|
|
6360
6581
|
},
|
|
6361
|
-
|
|
6582
|
+
// Enumerable so `{ ...stack }` keeps `read`, `flows` and `readArgCounts`.
|
|
6583
|
+
enumerable: true,
|
|
6362
6584
|
configurable: false,
|
|
6363
6585
|
writable: false,
|
|
6364
6586
|
});
|
|
@@ -6396,7 +6618,7 @@ function isPreparedInstruction(instruction) {
|
|
|
6396
6618
|
&& instruction.kind === 'instruction'
|
|
6397
6619
|
&& 'instruction' in instruction;
|
|
6398
6620
|
}
|
|
6399
|
-
function nonEmpty(values, label) {
|
|
6621
|
+
function nonEmpty$1(values, label) {
|
|
6400
6622
|
if (values.length === 0) {
|
|
6401
6623
|
throw new Error(`${label} must contain at least one item`);
|
|
6402
6624
|
}
|
|
@@ -6409,7 +6631,7 @@ function inferSignerAddresses(instructions) {
|
|
|
6409
6631
|
return dedupe(instructions.flatMap((instruction) => instruction.keys.filter((key) => key.isSigner).map((key) => key.pubkey)));
|
|
6410
6632
|
}
|
|
6411
6633
|
function createPreparedTransactionBody(input) {
|
|
6412
|
-
const instructions = nonEmpty(input.instructions, `Transaction '${input.name}'`);
|
|
6634
|
+
const instructions = nonEmpty$1(input.instructions, `Transaction '${input.name}'`);
|
|
6413
6635
|
return {
|
|
6414
6636
|
name: input.name,
|
|
6415
6637
|
instructions,
|
|
@@ -6422,7 +6644,7 @@ function createPlan(name, artifacts, transactions) {
|
|
|
6422
6644
|
return {
|
|
6423
6645
|
name,
|
|
6424
6646
|
artifacts,
|
|
6425
|
-
transactions: nonEmpty(transactions, `Flow '${name}'`),
|
|
6647
|
+
transactions: nonEmpty$1(transactions, `Flow '${name}'`),
|
|
6426
6648
|
};
|
|
6427
6649
|
}
|
|
6428
6650
|
function createPreparedInstruction(input) {
|
|
@@ -6708,8 +6930,8 @@ async function executePreparedOperation(host, operation, options = {}) {
|
|
|
6708
6930
|
kind: 'flow',
|
|
6709
6931
|
operationName: operation.name,
|
|
6710
6932
|
artifacts: operation.artifacts,
|
|
6711
|
-
signatures: nonEmpty(receipts.map((receipt) => receipt.signature), `Operation '${operation.name}' signatures`),
|
|
6712
|
-
transactions: nonEmpty(receipts, `Operation '${operation.name}' receipts`),
|
|
6933
|
+
signatures: nonEmpty$1(receipts.map((receipt) => receipt.signature), `Operation '${operation.name}' signatures`),
|
|
6934
|
+
transactions: nonEmpty$1(receipts, `Operation '${operation.name}' receipts`),
|
|
6713
6935
|
...callbackErrorResult,
|
|
6714
6936
|
};
|
|
6715
6937
|
}
|
|
@@ -6985,6 +7207,127 @@ function createProgramReadTransport(config) {
|
|
|
6985
7207
|
};
|
|
6986
7208
|
}
|
|
6987
7209
|
|
|
7210
|
+
/** Error code for two different program SDKs claiming one program key. */
|
|
7211
|
+
const PROGRAM_KEY_CONFLICT = 'PROGRAM_KEY_CONFLICT';
|
|
7212
|
+
function nonEmpty(value) {
|
|
7213
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
7214
|
+
}
|
|
7215
|
+
/** Compare two program definitions by identity, never by name. */
|
|
7216
|
+
function compareProgramIdentity(left, right) {
|
|
7217
|
+
if (left === right)
|
|
7218
|
+
return 'same';
|
|
7219
|
+
const leftRelease = nonEmpty(left.packageReleaseHash);
|
|
7220
|
+
const rightRelease = nonEmpty(right.packageReleaseHash);
|
|
7221
|
+
if (leftRelease !== undefined && rightRelease !== undefined) {
|
|
7222
|
+
return leftRelease === rightRelease ? 'same' : 'different';
|
|
7223
|
+
}
|
|
7224
|
+
const leftSpec = nonEmpty(left.programSpecHash);
|
|
7225
|
+
return leftSpec !== undefined && leftSpec === nonEmpty(right.programSpecHash)
|
|
7226
|
+
? 'unproven'
|
|
7227
|
+
: 'different';
|
|
7228
|
+
}
|
|
7229
|
+
/**
|
|
7230
|
+
* Whether two program definitions are provably the same program SDK: the same
|
|
7231
|
+
* object, or both carry the same `packageReleaseHash` (the program package
|
|
7232
|
+
* release they were generated from). Anything else is not provably the same,
|
|
7233
|
+
* even under the same name.
|
|
7234
|
+
*/
|
|
7235
|
+
function isSameProgramSdk(left, right) {
|
|
7236
|
+
return compareProgramIdentity(left, right) === 'same';
|
|
7237
|
+
}
|
|
7238
|
+
function identityOf(program) {
|
|
7239
|
+
const release = nonEmpty(program.packageReleaseHash);
|
|
7240
|
+
if (release !== undefined)
|
|
7241
|
+
return `package release ${release}`;
|
|
7242
|
+
const spec = nonEmpty(program.programSpecHash);
|
|
7243
|
+
return spec !== undefined
|
|
7244
|
+
? `program spec ${spec}, no package release`
|
|
7245
|
+
: 'no program identity';
|
|
7246
|
+
}
|
|
7247
|
+
const warned = new Set();
|
|
7248
|
+
/** Emit a program identity warning once per distinct message. */
|
|
7249
|
+
function warnOnce(message) {
|
|
7250
|
+
if (warned.has(message))
|
|
7251
|
+
return;
|
|
7252
|
+
warned.add(message);
|
|
7253
|
+
console.warn(`[Arete] ${message}`);
|
|
7254
|
+
}
|
|
7255
|
+
/**
|
|
7256
|
+
* A program attached next to a stack (`withPrograms`, `ConnectOptions.programs`,
|
|
7257
|
+
* `useArete(stack, { programs })`, a session member's `programs`) under a key
|
|
7258
|
+
* the stack already provides with a different program SDK.
|
|
7259
|
+
*/
|
|
7260
|
+
function attachedProgramKeyConflict(stackName, key, stackProgram, attached) {
|
|
7261
|
+
return new AreteError(`Program key '${key}' conflicts with stack '${stackName}': the stack already provides `
|
|
7262
|
+
+ `a different '${key}' program SDK (stack: ${identityOf(stackProgram)}; attached: `
|
|
7263
|
+
+ `${identityOf(attached)}). Use the stack's program at programs.${key}, or attach the `
|
|
7264
|
+
+ `other program under a different key, one the stack does not use.`, PROGRAM_KEY_CONFLICT, { key, stack: stackName });
|
|
7265
|
+
}
|
|
7266
|
+
/**
|
|
7267
|
+
* An attached program replaces the stack's program under one key without a
|
|
7268
|
+
* provable identity match (same program spec, no shared package release).
|
|
7269
|
+
*/
|
|
7270
|
+
function warnUnprovenAttachedProgram(stackName, key, stackProgram, attached) {
|
|
7271
|
+
warnOnce(`programs.${key} uses the program attached to stack '${stackName}': it has the same `
|
|
7272
|
+
+ `program spec as the stack's '${key}' program but could not be proven identical `
|
|
7273
|
+
+ `(stack: ${identityOf(stackProgram)}; attached: ${identityOf(attached)}).`);
|
|
7274
|
+
}
|
|
7275
|
+
/** A standalone session program under a key a session stack already provides. */
|
|
7276
|
+
function sessionProgramKeyConflict(stackKey, key, stackProgram, standalone) {
|
|
7277
|
+
return new AreteError(`Program key '${key}' conflicts with stack '${stackKey}' in this session: the stack `
|
|
7278
|
+
+ `already provides a different '${key}' program SDK (stack: ${identityOf(stackProgram)}; `
|
|
7279
|
+
+ `standalone: ${identityOf(standalone)}). Use session.stacks.${stackKey}.programs.${key}, `
|
|
7280
|
+
+ `or attach the standalone program under a different key in createSession({ programs }).`, PROGRAM_KEY_CONFLICT, { key, stack: stackKey });
|
|
7281
|
+
}
|
|
7282
|
+
/**
|
|
7283
|
+
* A standalone session program takes the top-level key from stacks whose
|
|
7284
|
+
* program it could not be proven identical to: a different identity
|
|
7285
|
+
* (`'identity'`), or the same program SDK read through a different program
|
|
7286
|
+
* read configuration than the stack's (`'program-read'`).
|
|
7287
|
+
*/
|
|
7288
|
+
function warnUnprovenSessionProgram(key, standalone, providers, cause = 'identity') {
|
|
7289
|
+
const stacks = providers.map(({ stackKey }) => `'${stackKey}'`).join(' and ');
|
|
7290
|
+
const paths = providers
|
|
7291
|
+
.map(({ stackKey }) => `session.stacks.${stackKey}.programs.${key}`)
|
|
7292
|
+
.join(' and ');
|
|
7293
|
+
const identities = `standalone: ${identityOf(standalone)}; ${providers
|
|
7294
|
+
.map(({ stackKey, program }) => `${stackKey}: ${identityOf(program)}`)
|
|
7295
|
+
.join('; ')}`;
|
|
7296
|
+
const reason = cause === 'program-read'
|
|
7297
|
+
? `it is the same program SDK as the '${key}' program of stack ${stacks} (${identities}) `
|
|
7298
|
+
+ `but its programReads.${key} descriptor differs from the one the stack reads through, `
|
|
7299
|
+
+ `so it keeps its own program read configuration`
|
|
7300
|
+
: `it has the same program spec as the '${key}' program of stack ${stacks} but could not `
|
|
7301
|
+
+ `be proven identical (${identities})`;
|
|
7302
|
+
warnOnce(`session.programs.${key} uses the standalone program: ${reason}. `
|
|
7303
|
+
+ `${paths} keep${providers.length === 1 ? 's' : ''} the stack's program.`);
|
|
7304
|
+
}
|
|
7305
|
+
/**
|
|
7306
|
+
* Several session stacks bundle one key with the same program spec, not all
|
|
7307
|
+
* provably the same SDK; the first stack's program takes the top-level key.
|
|
7308
|
+
*/
|
|
7309
|
+
function warnUnprovenStackPrograms(key, providers) {
|
|
7310
|
+
const first = providers[0];
|
|
7311
|
+
warnOnce(`session.programs.${key} uses stack '${first.stackKey}''s program: stacks `
|
|
7312
|
+
+ `${providers.map(({ stackKey }) => `'${stackKey}'`).join(' and ')} bundle '${key}' `
|
|
7313
|
+
+ `programs with the same program spec that could not be proven identical (${providers
|
|
7314
|
+
.map(({ stackKey, program }) => `${stackKey}: ${identityOf(program)}`)
|
|
7315
|
+
.join('; ')}). Each stays at session.stacks.<stack>.programs.${key}.`);
|
|
7316
|
+
}
|
|
7317
|
+
/** Two session stacks provide different program SDKs under one key. */
|
|
7318
|
+
function ambiguousSessionProgram(key, providers) {
|
|
7319
|
+
const stacks = providers.map(({ stackKey }) => `'${stackKey}'`).join(' and ');
|
|
7320
|
+
const paths = providers
|
|
7321
|
+
.map(({ stackKey }) => `session.stacks.${stackKey}.programs.${key}`)
|
|
7322
|
+
.join(' or ');
|
|
7323
|
+
const identities = providers
|
|
7324
|
+
.map(({ stackKey, program }) => `${stackKey}: ${identityOf(program)}`)
|
|
7325
|
+
.join('; ');
|
|
7326
|
+
return new AreteError(`session.programs.${key} is ambiguous: stacks ${stacks} provide different '${key}' `
|
|
7327
|
+
+ `program SDKs (${identities}). Use ${paths}, or attach the one you want under a `
|
|
7328
|
+
+ `different key in createSession({ programs }).`, PROGRAM_KEY_CONFLICT, { key, stacks: providers.map(({ stackKey }) => stackKey) });
|
|
7329
|
+
}
|
|
7330
|
+
|
|
6988
7331
|
function effectiveProgramRead(stack, name, override) {
|
|
6989
7332
|
return override
|
|
6990
7333
|
?? stack.programReads?.[name]
|
|
@@ -7092,13 +7435,38 @@ function validateLocalProgramReadEndpoints(stack, overrides, connectHttpUrl) {
|
|
|
7092
7435
|
function hasProgramAccountReads(definition) {
|
|
7093
7436
|
return Object.keys(definition.accounts ?? {}).length > 0;
|
|
7094
7437
|
}
|
|
7438
|
+
/**
|
|
7439
|
+
* Attach programs next to a stack's own, matching a key the stack already
|
|
7440
|
+
* provides by program identity (see `compareProgramIdentity`):
|
|
7441
|
+
*
|
|
7442
|
+
* - the same program SDK (same object, or equal `packageReleaseHash`) resolves
|
|
7443
|
+
* to the stack's definition;
|
|
7444
|
+
* - the same `programSpecHash` without a provable identity match (at least one
|
|
7445
|
+
* side has no `packageReleaseHash`) uses the attached program, with one
|
|
7446
|
+
* warning;
|
|
7447
|
+
* - anything else throws `PROGRAM_KEY_CONFLICT`.
|
|
7448
|
+
*/
|
|
7095
7449
|
function mergeAttachedPrograms(stack, attachedPrograms) {
|
|
7096
7450
|
const merged = { ...(attachedPrograms ?? {}) };
|
|
7097
7451
|
for (const [name, definition] of Object.entries(stack.programs ?? {})) {
|
|
7098
|
-
|
|
7099
|
-
|
|
7452
|
+
const attached = attachedPrograms && Object.prototype.hasOwnProperty.call(attachedPrograms, name)
|
|
7453
|
+
? attachedPrograms[name]
|
|
7454
|
+
: undefined;
|
|
7455
|
+
if (attached === undefined) {
|
|
7456
|
+
merged[name] = definition;
|
|
7457
|
+
continue;
|
|
7458
|
+
}
|
|
7459
|
+
switch (compareProgramIdentity(definition, attached)) {
|
|
7460
|
+
case 'same':
|
|
7461
|
+
merged[name] = definition;
|
|
7462
|
+
break;
|
|
7463
|
+
case 'unproven':
|
|
7464
|
+
warnUnprovenAttachedProgram(stack.name, name, definition, attached);
|
|
7465
|
+
merged[name] = attached;
|
|
7466
|
+
break;
|
|
7467
|
+
case 'different':
|
|
7468
|
+
throw attachedProgramKeyConflict(stack.name, name, definition, attached);
|
|
7100
7469
|
}
|
|
7101
|
-
merged[name] = definition;
|
|
7102
7470
|
}
|
|
7103
7471
|
return merged;
|
|
7104
7472
|
}
|
|
@@ -7698,6 +8066,34 @@ function resolveSessionProgramReads(stack, member, options) {
|
|
|
7698
8066
|
}));
|
|
7699
8067
|
return Object.keys(reads).length > 0 ? reads : undefined;
|
|
7700
8068
|
}
|
|
8069
|
+
/**
|
|
8070
|
+
* The program read descriptor a session member connected for `stack` reads
|
|
8071
|
+
* `programKey` through, resolved the way `Arete.connect` resolves it: session
|
|
8072
|
+
* and member overrides, then the stack's `programReads`, then the descriptor
|
|
8073
|
+
* the program carries.
|
|
8074
|
+
*/
|
|
8075
|
+
function memberProgramRead(stack, programKey, member, options) {
|
|
8076
|
+
return resolveSessionProgramReads(stack, member, options)?.[programKey]
|
|
8077
|
+
?? stack.programReads?.[programKey]
|
|
8078
|
+
?? getProgramReadDescriptor(stack.programs?.[programKey]);
|
|
8079
|
+
}
|
|
8080
|
+
/** Structural equality of two program read descriptors (plain JSON data). */
|
|
8081
|
+
function sameProgramRead(left, right) {
|
|
8082
|
+
if (left === right)
|
|
8083
|
+
return true;
|
|
8084
|
+
if (left === null || right === null
|
|
8085
|
+
|| typeof left !== 'object' || typeof right !== 'object'
|
|
8086
|
+
|| Array.isArray(left) !== Array.isArray(right)) {
|
|
8087
|
+
return false;
|
|
8088
|
+
}
|
|
8089
|
+
const leftRecord = left;
|
|
8090
|
+
const rightRecord = right;
|
|
8091
|
+
const leftKeys = Object.keys(leftRecord).filter((key) => leftRecord[key] !== undefined);
|
|
8092
|
+
const rightKeys = Object.keys(rightRecord).filter((key) => rightRecord[key] !== undefined);
|
|
8093
|
+
return leftKeys.length === rightKeys.length
|
|
8094
|
+
&& leftKeys.every((key) => Object.prototype.hasOwnProperty.call(rightRecord, key)
|
|
8095
|
+
&& sameProgramRead(leftRecord[key], rightRecord[key]));
|
|
8096
|
+
}
|
|
7701
8097
|
function resolveMemberConnectOptions(stack, member, options, forceHttpOnly) {
|
|
7702
8098
|
const transport = member?.transport ?? options?.transport ?? (forceHttpOnly ? 'http' : 'ws');
|
|
7703
8099
|
// Stack endpoints remain on the stack definition. Only explicit session/member
|
|
@@ -7791,11 +8187,72 @@ async function createSession(definition, ...args) {
|
|
|
7791
8187
|
validateCompositionProgramReads(programAsStack(key, program, definition.programReads?.[key]), options?.programs?.[key], options);
|
|
7792
8188
|
}
|
|
7793
8189
|
}
|
|
8190
|
+
// Program identity, resolved before anything connects. Every stack's
|
|
8191
|
+
// effective programs (its own plus member-attached ones, matched by
|
|
8192
|
+
// `withPrograms`) are the providers for each key.
|
|
8193
|
+
const effectiveStacks = new Map(stackEntries.map(([key, stack]) => {
|
|
8194
|
+
const memberOptions = options?.stacks?.[key];
|
|
8195
|
+
return [key, withPrograms(stack, memberOptions?.programs)];
|
|
8196
|
+
}));
|
|
8197
|
+
const stackProviders = new Map();
|
|
8198
|
+
for (const [stackKey, stack] of effectiveStacks) {
|
|
8199
|
+
for (const [programKey, program] of Object.entries(stack.programs ?? {})) {
|
|
8200
|
+
const providers = stackProviders.get(programKey) ?? [];
|
|
8201
|
+
providers.push({ stackKey, program });
|
|
8202
|
+
stackProviders.set(programKey, providers);
|
|
8203
|
+
}
|
|
8204
|
+
}
|
|
8205
|
+
// Standalone programs a stack already provides as the same program SDK, read
|
|
8206
|
+
// through the same program read configuration, are served by that stack's
|
|
8207
|
+
// connected instance instead of a second member. One with the same program
|
|
8208
|
+
// spec but no provable identity match, or the same program SDK with its own
|
|
8209
|
+
// `programReads` descriptor that differs from the stack's, gets its own
|
|
8210
|
+
// member and takes session.programs.<key>; the stacks keep theirs.
|
|
8211
|
+
const sharedStandalonePrograms = new Set();
|
|
8212
|
+
if (definition.mode !== 'composition') {
|
|
8213
|
+
for (const [programKey, program] of programEntries) {
|
|
8214
|
+
const providers = stackProviders.get(programKey) ?? [];
|
|
8215
|
+
if (providers.length === 0)
|
|
8216
|
+
continue;
|
|
8217
|
+
const matches = providers.map((provider) => ({
|
|
8218
|
+
...provider,
|
|
8219
|
+
match: compareProgramIdentity(provider.program, program),
|
|
8220
|
+
}));
|
|
8221
|
+
const conflicting = matches.find(({ match }) => match === 'different');
|
|
8222
|
+
if (conflicting) {
|
|
8223
|
+
throw sessionProgramKeyConflict(conflicting.stackKey, programKey, conflicting.program, program);
|
|
8224
|
+
}
|
|
8225
|
+
if (matches.some(({ match }) => match === 'unproven')) {
|
|
8226
|
+
warnUnprovenSessionProgram(programKey, program, providers);
|
|
8227
|
+
continue;
|
|
8228
|
+
}
|
|
8229
|
+
const explicitRead = definition.programReads?.[programKey];
|
|
8230
|
+
if (explicitRead !== undefined) {
|
|
8231
|
+
// Sharing would read through the serving stack's configuration, so an
|
|
8232
|
+
// explicit descriptor must resolve to the same one.
|
|
8233
|
+
const servingStack = providers[0].stackKey;
|
|
8234
|
+
const standaloneRead = memberProgramRead(programAsStack(programKey, program, explicitRead), programKey, options?.programs?.[programKey], options);
|
|
8235
|
+
const stackRead = memberProgramRead(effectiveStacks.get(servingStack), programKey, options?.stacks?.[servingStack], options);
|
|
8236
|
+
if (!sameProgramRead(standaloneRead, stackRead)) {
|
|
8237
|
+
warnUnprovenSessionProgram(programKey, program, providers, 'program-read');
|
|
8238
|
+
continue;
|
|
8239
|
+
}
|
|
8240
|
+
}
|
|
8241
|
+
if (options?.programs?.[programKey] !== undefined) {
|
|
8242
|
+
throw new AreteError(`Standalone program '${programKey}' is the same program SDK that stack `
|
|
8243
|
+
+ `'${providers[0].stackKey}' provides, so the session serves it from the stack's `
|
|
8244
|
+
+ `connected instance; configure it through options.stacks.${providers[0].stackKey} `
|
|
8245
|
+
+ `instead of options.programs.${programKey}`, 'INVALID_CONFIG');
|
|
8246
|
+
}
|
|
8247
|
+
sharedStandalonePrograms.add(programKey);
|
|
8248
|
+
}
|
|
8249
|
+
}
|
|
8250
|
+
const memberProgramEntries = programEntries.filter(([key]) => !sharedStandalonePrograms.has(key));
|
|
7794
8251
|
let wallet = options?.wallet;
|
|
7795
8252
|
const signerRegistry = options?.signerRegistry ?? createSignerRegistry();
|
|
7796
8253
|
const connectedStacks = await Promise.all(stackEntries.map(async ([key, stack]) => {
|
|
7797
8254
|
const memberOptions = options?.stacks?.[key];
|
|
7798
|
-
const effectiveStack =
|
|
8255
|
+
const effectiveStack = effectiveStacks.get(key);
|
|
7799
8256
|
const connectOptions = resolveMemberConnectOptions(effectiveStack, memberOptions, {
|
|
7800
8257
|
...options,
|
|
7801
8258
|
chain: sharedChain,
|
|
@@ -7808,7 +8265,7 @@ async function createSession(definition, ...args) {
|
|
|
7808
8265
|
});
|
|
7809
8266
|
return [key, client];
|
|
7810
8267
|
}));
|
|
7811
|
-
const connectedPrograms = await Promise.all(
|
|
8268
|
+
const connectedPrograms = await Promise.all(memberProgramEntries.map(async ([key, program]) => {
|
|
7812
8269
|
const syntheticStack = programAsStack(key, program, definition.programReads?.[key]);
|
|
7813
8270
|
const connectOptions = resolveMemberConnectOptions(syntheticStack, options?.programs?.[key], {
|
|
7814
8271
|
...options,
|
|
@@ -7825,30 +8282,38 @@ async function createSession(definition, ...args) {
|
|
|
7825
8282
|
const executionHost = memberClients[0];
|
|
7826
8283
|
const transactions = sharedTransactions ?? executionHost.transactions;
|
|
7827
8284
|
const stacks = Object.fromEntries(connectedStacks);
|
|
7828
|
-
const explicitProgramKeys = new Set(connectedPrograms.map(([key]) => key));
|
|
7829
|
-
const programOwners = new Map();
|
|
7830
8285
|
const connectedProgramEntries = connectedPrograms.map(([key, client]) => [
|
|
7831
8286
|
key,
|
|
7832
8287
|
client.programs[key],
|
|
7833
8288
|
]);
|
|
7834
8289
|
const promotedPrograms = Object.fromEntries(connectedProgramEntries);
|
|
7835
8290
|
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 });
|
|
8291
|
+
const connectedStackClients = new Map(connectedStacks);
|
|
8292
|
+
for (const [programKey, providers] of stackProviders) {
|
|
8293
|
+
if (Object.prototype.hasOwnProperty.call(promotedPrograms, programKey))
|
|
8294
|
+
continue;
|
|
8295
|
+
const first = providers[0];
|
|
8296
|
+
const matches = providers.flatMap((left, index) => providers.slice(index + 1).map((right) => compareProgramIdentity(left.program, right.program)));
|
|
8297
|
+
if (!matches.includes('different')) {
|
|
8298
|
+
// One program, however many stacks bundle it: the first stack's
|
|
8299
|
+
// connected instance serves the top-level key. Copies with the same
|
|
8300
|
+
// program spec that cannot be proven identical say so once.
|
|
8301
|
+
if (matches.includes('unproven'))
|
|
8302
|
+
warnUnprovenStackPrograms(programKey, providers);
|
|
8303
|
+
promotedPrograms[programKey] = connectedStackClients.get(first.stackKey).programs[programKey];
|
|
8304
|
+
continue;
|
|
7851
8305
|
}
|
|
8306
|
+
// Different program SDKs under one key: every stack keeps its own at
|
|
8307
|
+
// session.stacks.<stack>.programs.<key>, and the ambiguous top-level key
|
|
8308
|
+
// explains how to choose instead of silently picking one.
|
|
8309
|
+
const error = ambiguousSessionProgram(programKey, providers);
|
|
8310
|
+
Object.defineProperty(promotedPrograms, programKey, {
|
|
8311
|
+
get() {
|
|
8312
|
+
throw error;
|
|
8313
|
+
},
|
|
8314
|
+
enumerable: false,
|
|
8315
|
+
configurable: true,
|
|
8316
|
+
});
|
|
7852
8317
|
}
|
|
7853
8318
|
}
|
|
7854
8319
|
const programs = promotedPrograms;
|
|
@@ -8090,12 +8555,14 @@ exports.AreteError = AreteError;
|
|
|
8090
8555
|
exports.ConnectionManager = ConnectionManager;
|
|
8091
8556
|
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
|
8092
8557
|
exports.DEFAULT_MAX_ENTRIES_PER_VIEW = DEFAULT_MAX_ENTRIES_PER_VIEW;
|
|
8558
|
+
exports.EXTENSION_API_VERSION = EXTENSION_API_VERSION;
|
|
8093
8559
|
exports.FrameProcessor = FrameProcessor;
|
|
8094
8560
|
exports.InstructionError = InstructionError;
|
|
8095
8561
|
exports.MemoryAdapter = MemoryAdapter;
|
|
8096
8562
|
exports.OPERATION_KIND = OPERATION_KIND;
|
|
8097
8563
|
exports.OperationCallbackError = OperationCallbackError;
|
|
8098
8564
|
exports.OperationExecutionError = OperationExecutionError;
|
|
8565
|
+
exports.PROGRAM_KEY_CONFLICT = PROGRAM_KEY_CONFLICT;
|
|
8099
8566
|
exports.PROGRAM_OPERATION_EXTENSIONS = PROGRAM_OPERATION_EXTENSIONS;
|
|
8100
8567
|
exports.ProcessedSlotTimeoutError = ProcessedSlotTimeoutError;
|
|
8101
8568
|
exports.QueryStore = QueryStore;
|
|
@@ -8119,6 +8586,7 @@ exports.buildInstruction = buildInstruction;
|
|
|
8119
8586
|
exports.bytes = bytes;
|
|
8120
8587
|
exports.canonicalQueryKey = canonicalQueryKey;
|
|
8121
8588
|
exports.chainAccountLoader = chainAccountLoader;
|
|
8589
|
+
exports.compareProgramIdentity = compareProgramIdentity;
|
|
8122
8590
|
exports.createChainClient = createChainClient;
|
|
8123
8591
|
exports.createEntityStream = createEntityStream;
|
|
8124
8592
|
exports.createHostedSolanaGatewayTransports = createHostedSolanaGatewayTransports;
|
|
@@ -8171,6 +8639,7 @@ exports.isEntityFrame = isEntityFrame;
|
|
|
8171
8639
|
exports.isErrorFrame = isErrorFrame;
|
|
8172
8640
|
exports.isHostedAreteEndpoint = isHostedAreteEndpoint;
|
|
8173
8641
|
exports.isReplayErrorCode = isReplayErrorCode;
|
|
8642
|
+
exports.isSameProgramSdk = isSameProgramSdk;
|
|
8174
8643
|
exports.isSnapshotFrame = isSnapshotFrame;
|
|
8175
8644
|
exports.isStackVersionRefusalCode = isStackVersionRefusalCode;
|
|
8176
8645
|
exports.isSubscribedFrame = isSubscribedFrame;
|
|
@@ -8210,6 +8679,7 @@ exports.unwrapOperationExecutionError = unwrapOperationExecutionError;
|
|
|
8210
8679
|
exports.validateAccountResolution = validateAccountResolution;
|
|
8211
8680
|
exports.validateProgramReadDescriptor = validateProgramReadDescriptor;
|
|
8212
8681
|
exports.validateSubscriptionId = validateSubscriptionId;
|
|
8682
|
+
exports.withProgramIdentity = withProgramIdentity;
|
|
8213
8683
|
exports.withProgramRead = withProgramRead;
|
|
8214
8684
|
exports.withPrograms = withPrograms;
|
|
8215
8685
|
//# sourceMappingURL=index.cjs.map
|