@myko/core 6.0.0 → 6.2.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/dist/client.d.ts +114 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +354 -25
- package/dist/generated/AdjacencyPolicy.d.ts +2 -0
- package/dist/generated/AdjacencyPolicy.d.ts.map +1 -0
- package/dist/generated/AdjacencyPolicy.js +2 -0
- package/dist/generated/Direction.d.ts +2 -0
- package/dist/generated/Direction.d.ts.map +1 -0
- package/dist/generated/Direction.js +2 -0
- package/dist/generated/EdgeShapeKind.d.ts +2 -0
- package/dist/generated/EdgeShapeKind.d.ts.map +1 -0
- package/dist/generated/EdgeShapeKind.js +2 -0
- package/dist/generated/EndPosition.d.ts +2 -0
- package/dist/generated/EndPosition.d.ts.map +1 -0
- package/dist/generated/EndPosition.js +2 -0
- package/dist/generated/EndpointDeletePolicy.d.ts +2 -0
- package/dist/generated/EndpointDeletePolicy.d.ts.map +1 -0
- package/dist/generated/EndpointDeletePolicy.js +2 -0
- package/dist/generated/EntityRef.d.ts +8 -0
- package/dist/generated/EntityRef.d.ts.map +1 -0
- package/dist/generated/EntityRef.js +2 -0
- package/dist/generated/GraphPlanTelemetry.d.ts +16 -0
- package/dist/generated/GraphPlanTelemetry.d.ts.map +1 -0
- package/dist/generated/GraphPlanTelemetry.js +1 -0
- package/dist/generated/GraphSyncResult.d.ts +10 -0
- package/dist/generated/GraphSyncResult.d.ts.map +1 -0
- package/dist/generated/GraphSyncResult.js +2 -0
- package/dist/generated/MykoMessage.d.ts +4 -0
- package/dist/generated/MykoMessage.d.ts.map +1 -1
- package/dist/generated/PairPolicy.d.ts +2 -0
- package/dist/generated/PairPolicy.d.ts.map +1 -0
- package/dist/generated/PairPolicy.js +2 -0
- package/dist/generated/PairProjectionPolicy.d.ts +2 -0
- package/dist/generated/PairProjectionPolicy.d.ts.map +1 -0
- package/dist/generated/PairProjectionPolicy.js +2 -0
- package/dist/generated/QueryCursorWindow.d.ts +12 -0
- package/dist/generated/QueryCursorWindow.d.ts.map +1 -0
- package/dist/generated/QueryCursorWindow.js +2 -0
- package/dist/generated/QueryCursorWindowUpdate.d.ts +6 -0
- package/dist/generated/QueryCursorWindowUpdate.d.ts.map +1 -0
- package/dist/generated/QueryCursorWindowUpdate.js +1 -0
- package/dist/generated/SelfLoopPolicy.d.ts +2 -0
- package/dist/generated/SelfLoopPolicy.d.ts.map +1 -0
- package/dist/generated/SelfLoopPolicy.js +2 -0
- package/dist/generated/TraversalPath.d.ts +12 -0
- package/dist/generated/TraversalPath.d.ts.map +1 -0
- package/dist/generated/TraversalPath.js +1 -0
- package/dist/generated/TraversalResult.d.ts +7 -0
- package/dist/generated/TraversalResult.d.ts.map +1 -0
- package/dist/generated/TraversalResult.js +1 -0
- package/dist/generated/index.d.ts +152 -130
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/generated/index.js +98 -95
- package/dist/graph-client.d.ts +128 -0
- package/dist/graph-client.d.ts.map +1 -0
- package/dist/graph-client.js +192 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/live-collection.d.ts +94 -0
- package/dist/live-collection.d.ts.map +1 -0
- package/dist/live-collection.js +302 -0
- package/package.json +5 -2
package/dist/client.js
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* Maintains connections to all known servers for instant failover.
|
|
5
5
|
* When the current server disconnects, instantly switches to another open connection.
|
|
6
6
|
*/
|
|
7
|
-
import { GetPeerServers, MykoEvent, } from './generated';
|
|
8
7
|
import { Decoder, Encoder } from 'cbor-x';
|
|
9
|
-
import { bufferCount, bufferTime, catchError, combineLatest, filter, finalize, firstValueFrom, interval, map, merge, Observable, of, ReplaySubject, scan, shareReplay,
|
|
8
|
+
import { bufferCount, bufferTime, catchError, combineLatest, distinctUntilChanged, filter, finalize, firstValueFrom, interval, map, merge, Observable, of, ReplaySubject, Subject, scan, shareReplay, switchMap, } from 'rxjs';
|
|
10
9
|
import { v4 as uuid } from 'uuid';
|
|
10
|
+
import { GetPeerServers, MykoEvent, } from './generated';
|
|
11
|
+
import { bindGraph } from './graph-client.js';
|
|
12
|
+
import { LiveCollection } from './live-collection.js';
|
|
11
13
|
// cbor-x's defaults emit several non-standard extensions (the "records" structure
|
|
12
14
|
// compression, tag 259 for maps with non-string keys, typed-array tags) that other
|
|
13
15
|
// CBOR implementations — including the Rust server's `ciborium` decoder targeting
|
|
@@ -156,6 +158,77 @@ function reportCacheKey(report) {
|
|
|
156
158
|
const payload = stableStringify(report.report) ?? '__unstable_report__';
|
|
157
159
|
return `report:${report.reportId}:${payload}`;
|
|
158
160
|
}
|
|
161
|
+
/** One shared source subscription with direct fine-grained state listeners. */
|
|
162
|
+
class LiveSelectionHub {
|
|
163
|
+
#source;
|
|
164
|
+
#onEmpty;
|
|
165
|
+
#registrations = new Set();
|
|
166
|
+
#state;
|
|
167
|
+
#sourceSubscription;
|
|
168
|
+
constructor(source, onEmpty) {
|
|
169
|
+
this.#source = source;
|
|
170
|
+
this.#onEmpty = onEmpty;
|
|
171
|
+
}
|
|
172
|
+
watch(register, equals = Object.is) {
|
|
173
|
+
return new Observable((subscriber) => {
|
|
174
|
+
let emitted = false;
|
|
175
|
+
let previous;
|
|
176
|
+
const emit = (value) => {
|
|
177
|
+
if (emitted && equals(previous, value))
|
|
178
|
+
return;
|
|
179
|
+
emitted = true;
|
|
180
|
+
previous = value;
|
|
181
|
+
subscriber.next(value);
|
|
182
|
+
};
|
|
183
|
+
const registration = {
|
|
184
|
+
attach: (state) => {
|
|
185
|
+
registration.detach = register(state, emit);
|
|
186
|
+
},
|
|
187
|
+
complete: () => subscriber.complete(),
|
|
188
|
+
error: (cause) => subscriber.error(cause),
|
|
189
|
+
};
|
|
190
|
+
this.#registrations.add(registration);
|
|
191
|
+
if (this.#state !== undefined)
|
|
192
|
+
registration.attach(this.#state);
|
|
193
|
+
if (!this.#sourceSubscription)
|
|
194
|
+
this.#start();
|
|
195
|
+
return () => {
|
|
196
|
+
registration.detach?.();
|
|
197
|
+
this.#registrations.delete(registration);
|
|
198
|
+
if (this.#registrations.size === 0) {
|
|
199
|
+
this.#sourceSubscription?.unsubscribe();
|
|
200
|
+
this.#sourceSubscription = undefined;
|
|
201
|
+
this.#state = undefined;
|
|
202
|
+
this.#onEmpty();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
#start() {
|
|
208
|
+
const subscription = this.#source().subscribe({
|
|
209
|
+
next: (state) => {
|
|
210
|
+
if (this.#state === state)
|
|
211
|
+
return;
|
|
212
|
+
this.#state = state;
|
|
213
|
+
for (const registration of this.#registrations) {
|
|
214
|
+
registration.detach?.();
|
|
215
|
+
registration.attach(state);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
error: (cause) => {
|
|
219
|
+
for (const registration of [...this.#registrations]) {
|
|
220
|
+
registration.error(cause);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
complete: () => {
|
|
224
|
+
for (const registration of [...this.#registrations]) {
|
|
225
|
+
registration.complete();
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
this.#sourceSubscription = subscription.closed ? undefined : subscription;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
159
232
|
/**
|
|
160
233
|
* Reactive WebSocket client for Myko servers with automatic failover.
|
|
161
234
|
*/
|
|
@@ -187,6 +260,7 @@ export class MykoClient {
|
|
|
187
260
|
queryErrorRoutes = new Map();
|
|
188
261
|
viewErrorRoutes = new Map();
|
|
189
262
|
reportErrorRoutes = new Map();
|
|
263
|
+
pendingCommands = new Map();
|
|
190
264
|
// Lighter-weight direct-callback routing for `subscribeReport` — avoids
|
|
191
265
|
// allocating an RxJS Subject per anchor when the consumer just needs a
|
|
192
266
|
// single callback (which is the case for liveReport in the Svelte runtime).
|
|
@@ -197,6 +271,7 @@ export class MykoClient {
|
|
|
197
271
|
currentServerSubject = new ReplaySubject(1);
|
|
198
272
|
// Subscription tracking
|
|
199
273
|
activeQueries = new Map();
|
|
274
|
+
activeQueryCursorWindows = new Map();
|
|
200
275
|
activeViews = new Map();
|
|
201
276
|
activeReports = new Map();
|
|
202
277
|
activeQueryNames = new Map();
|
|
@@ -204,8 +279,14 @@ export class MykoClient {
|
|
|
204
279
|
activeReportNames = new Map();
|
|
205
280
|
sharedQueries = new Map();
|
|
206
281
|
sharedViews = new Map();
|
|
282
|
+
sharedQueryResponseStreams = new Map();
|
|
283
|
+
sharedViewResponseStreams = new Map();
|
|
207
284
|
sharedQueryDiffs = new Map();
|
|
208
285
|
sharedViewDiffs = new Map();
|
|
286
|
+
sharedQueryStates = new Map();
|
|
287
|
+
sharedViewStates = new Map();
|
|
288
|
+
querySelectionHubs = new Map();
|
|
289
|
+
viewSelectionHubs = new Map();
|
|
209
290
|
sharedReports = new Map();
|
|
210
291
|
subscriptionStartMs = new Map();
|
|
211
292
|
firstResponseLogged = new Set();
|
|
@@ -456,6 +537,7 @@ export class MykoClient {
|
|
|
456
537
|
activeQueriesBefore: this.activeQueries.size,
|
|
457
538
|
});
|
|
458
539
|
this.activeQueries.delete(tx);
|
|
540
|
+
this.activeQueryCursorWindows.delete(tx);
|
|
459
541
|
this.activeQueryNames.delete(tx);
|
|
460
542
|
this.subscriptionStartMs.delete(tx);
|
|
461
543
|
this.firstResponseLogged.delete(tx);
|
|
@@ -467,6 +549,29 @@ export class MykoClient {
|
|
|
467
549
|
}));
|
|
468
550
|
return [tx, responses$];
|
|
469
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* One ref-counted wire stream per immutable query identity.
|
|
554
|
+
*
|
|
555
|
+
* Array, diff, and keyed-state projections all consume this stream. Keeping
|
|
556
|
+
* the cache below those projections prevents the same query from opening
|
|
557
|
+
* several server subscriptions merely because different UI layers prefer
|
|
558
|
+
* different result shapes.
|
|
559
|
+
*/
|
|
560
|
+
sharedQueryResponses(query, options) {
|
|
561
|
+
const cacheKey = queryCacheKey(query, options);
|
|
562
|
+
const existing = this.sharedQueryResponseStreams.get(cacheKey);
|
|
563
|
+
if (existing)
|
|
564
|
+
return existing;
|
|
565
|
+
const [, responses$] = this.startQuery(query, options);
|
|
566
|
+
let shared$;
|
|
567
|
+
shared$ = responses$.pipe(finalize(() => {
|
|
568
|
+
if (this.sharedQueryResponseStreams.get(cacheKey) === shared$) {
|
|
569
|
+
this.sharedQueryResponseStreams.delete(cacheKey);
|
|
570
|
+
}
|
|
571
|
+
}), shareReplay({ bufferSize: 1, refCount: true }));
|
|
572
|
+
this.sharedQueryResponseStreams.set(cacheKey, shared$);
|
|
573
|
+
return shared$;
|
|
574
|
+
}
|
|
470
575
|
/** Update server-side window for an active query subscription */
|
|
471
576
|
setQueryWindow(tx, window) {
|
|
472
577
|
const active = this.activeQueries.get(tx);
|
|
@@ -489,6 +594,19 @@ export class MykoClient {
|
|
|
489
594
|
});
|
|
490
595
|
this.send({ event: MykoEvent.QueryWindow, data: { tx, window } });
|
|
491
596
|
}
|
|
597
|
+
/** Move an active query to an exclusive ID-keyset page. */
|
|
598
|
+
setQueryCursorWindow(tx, window) {
|
|
599
|
+
if (window.after !== undefined && window.before !== undefined) {
|
|
600
|
+
throw new Error('cursor window cannot contain both after and before');
|
|
601
|
+
}
|
|
602
|
+
if (!this.activeQueries.has(tx))
|
|
603
|
+
return;
|
|
604
|
+
this.activeQueryCursorWindows.set(tx, window);
|
|
605
|
+
this.send({
|
|
606
|
+
event: 'ws:m:query-cursor-window',
|
|
607
|
+
data: { tx, window },
|
|
608
|
+
});
|
|
609
|
+
}
|
|
492
610
|
/** Start a view subscription, returns [tx, responses$] */
|
|
493
611
|
startView(view, options) {
|
|
494
612
|
if (!view.viewId || !view.viewItemType || !view.view) {
|
|
@@ -558,6 +676,22 @@ export class MykoClient {
|
|
|
558
676
|
}));
|
|
559
677
|
return [tx, responses$];
|
|
560
678
|
}
|
|
679
|
+
/** One ref-counted wire stream shared by every immutable view projection. */
|
|
680
|
+
sharedViewResponses(view, options) {
|
|
681
|
+
const cacheKey = viewCacheKey(view, options);
|
|
682
|
+
const existing = this.sharedViewResponseStreams.get(cacheKey);
|
|
683
|
+
if (existing)
|
|
684
|
+
return existing;
|
|
685
|
+
const [, responses$] = this.startView(view, options);
|
|
686
|
+
let shared$;
|
|
687
|
+
shared$ = responses$.pipe(finalize(() => {
|
|
688
|
+
if (this.sharedViewResponseStreams.get(cacheKey) === shared$) {
|
|
689
|
+
this.sharedViewResponseStreams.delete(cacheKey);
|
|
690
|
+
}
|
|
691
|
+
}), shareReplay({ bufferSize: 1, refCount: true }));
|
|
692
|
+
this.sharedViewResponseStreams.set(cacheKey, shared$);
|
|
693
|
+
return shared$;
|
|
694
|
+
}
|
|
561
695
|
/** Update server-side window for an active view subscription */
|
|
562
696
|
setViewWindow(tx, window) {
|
|
563
697
|
const active = this.activeViews.get(tx);
|
|
@@ -585,7 +719,7 @@ export class MykoClient {
|
|
|
585
719
|
const existing = this.sharedQueries.get(cacheKey);
|
|
586
720
|
if (existing)
|
|
587
721
|
return existing;
|
|
588
|
-
const
|
|
722
|
+
const responses$ = this.sharedQueryResponses(query, options);
|
|
589
723
|
const shared$ = responses$.pipe(scan((acc, update) => {
|
|
590
724
|
if (BigInt(update.data.sequence) === 0n)
|
|
591
725
|
acc.clear();
|
|
@@ -613,7 +747,7 @@ export class MykoClient {
|
|
|
613
747
|
const existing = this.sharedViews.get(cacheKey);
|
|
614
748
|
if (existing)
|
|
615
749
|
return existing;
|
|
616
|
-
const
|
|
750
|
+
const responses$ = this.sharedViewResponses(view, options);
|
|
617
751
|
const shared$ = responses$.pipe(scan((acc, update) => {
|
|
618
752
|
if (BigInt(update.data.sequence) === 0n)
|
|
619
753
|
acc.clear();
|
|
@@ -637,7 +771,7 @@ export class MykoClient {
|
|
|
637
771
|
const existing = this.sharedQueryDiffs.get(cacheKey);
|
|
638
772
|
if (existing)
|
|
639
773
|
return existing;
|
|
640
|
-
const
|
|
774
|
+
const responses$ = this.sharedQueryResponses(query, options);
|
|
641
775
|
const shared$ = responses$.pipe(map((r) => ({
|
|
642
776
|
sequence: BigInt(r.data.sequence),
|
|
643
777
|
deletes: r.data.deletes.slice(),
|
|
@@ -658,7 +792,7 @@ export class MykoClient {
|
|
|
658
792
|
const existing = this.sharedViewDiffs.get(cacheKey);
|
|
659
793
|
if (existing)
|
|
660
794
|
return existing;
|
|
661
|
-
const
|
|
795
|
+
const responses$ = this.sharedViewResponses(view, options);
|
|
662
796
|
const shared$ = responses$.pipe(map((r) => ({
|
|
663
797
|
sequence: BigInt(r.data.sequence),
|
|
664
798
|
deletes: r.data.deletes.slice(),
|
|
@@ -673,6 +807,108 @@ export class MykoClient {
|
|
|
673
807
|
this.sharedViewDiffs.set(cacheKey, shared$);
|
|
674
808
|
return shared$;
|
|
675
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* Watch a query as stable keyed state without rebuilding its full result array.
|
|
812
|
+
*
|
|
813
|
+
* Identical requests share both the wire subscription and collection instance.
|
|
814
|
+
* Use `toArray()` only where array rendering is required; `get()`, `items`, and
|
|
815
|
+
* `changes` remain O(changes) for incremental updates.
|
|
816
|
+
*/
|
|
817
|
+
watchQueryState(query, options) {
|
|
818
|
+
const cacheKey = queryCacheKey(query, options);
|
|
819
|
+
const existing = this.sharedQueryStates.get(cacheKey);
|
|
820
|
+
if (existing)
|
|
821
|
+
return existing;
|
|
822
|
+
const state = new LiveCollection();
|
|
823
|
+
const shared$ = this.watchQueryDiff(query, options).pipe(map((diff) => state.apply(diff)), catchError((error) => of(state.fail(error))), finalize(() => {
|
|
824
|
+
this.sharedQueryStates.delete(cacheKey);
|
|
825
|
+
}), shareReplay({ bufferSize: 1, refCount: true }));
|
|
826
|
+
this.sharedQueryStates.set(cacheKey, shared$);
|
|
827
|
+
return shared$;
|
|
828
|
+
}
|
|
829
|
+
/** Watch a view as stable keyed state without full-array reconstruction. */
|
|
830
|
+
watchViewState(view, options) {
|
|
831
|
+
const cacheKey = viewCacheKey(view, options);
|
|
832
|
+
const existing = this.sharedViewStates.get(cacheKey);
|
|
833
|
+
if (existing)
|
|
834
|
+
return existing;
|
|
835
|
+
const state = new LiveCollection();
|
|
836
|
+
const shared$ = this.watchViewDiff(view, options).pipe(map((diff) => state.apply(diff)), catchError((error) => of(state.fail(error))), finalize(() => {
|
|
837
|
+
this.sharedViewStates.delete(cacheKey);
|
|
838
|
+
}), shareReplay({ bufferSize: 1, refCount: true }));
|
|
839
|
+
this.sharedViewStates.set(cacheKey, shared$);
|
|
840
|
+
return shared$;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Watch an immutable projection of keyed query state.
|
|
844
|
+
*
|
|
845
|
+
* The underlying query/state subscription remains shared. The selector runs
|
|
846
|
+
* once per source revision, while equal results suppress downstream work.
|
|
847
|
+
* Select scalars, entity references, or newly-created immutable values; do
|
|
848
|
+
* not return `state.items` directly because that Map is intentionally stable
|
|
849
|
+
* and mutates in place.
|
|
850
|
+
*/
|
|
851
|
+
watchQuerySelection(query, select, options) {
|
|
852
|
+
return this.watchQueryState(query, options).pipe(map(select), distinctUntilChanged(options?.equals ?? Object.is));
|
|
853
|
+
}
|
|
854
|
+
/** Watch one query result by ID without reacting to unrelated rows. */
|
|
855
|
+
watchQueryItem(query, id, options) {
|
|
856
|
+
return this.querySelectionHub(query, options).watch((state, emit) => state.subscribeItem(id, emit));
|
|
857
|
+
}
|
|
858
|
+
/** Watch membership of one ID without reacting to unrelated rows. */
|
|
859
|
+
watchQueryHas(query, id, options) {
|
|
860
|
+
return this.querySelectionHub(query, options).watch((state, emit) => state.subscribeItem(id, (item) => emit(item !== undefined)));
|
|
861
|
+
}
|
|
862
|
+
/** Watch live query cardinality without materializing its result array. */
|
|
863
|
+
watchQuerySize(query, options) {
|
|
864
|
+
return this.querySelectionHub(query, options).watch((state, emit) => state.subscribeSize(emit));
|
|
865
|
+
}
|
|
866
|
+
/** Watch an immutable projection of keyed view state. */
|
|
867
|
+
watchViewSelection(view, select, options) {
|
|
868
|
+
return this.watchViewState(view, options).pipe(map(select), distinctUntilChanged(options?.equals ?? Object.is));
|
|
869
|
+
}
|
|
870
|
+
/** Watch one view result by ID without reacting to unrelated rows. */
|
|
871
|
+
watchViewItem(view, id, options) {
|
|
872
|
+
return this.viewSelectionHub(view, options).watch((state, emit) => state.subscribeItem(id, emit));
|
|
873
|
+
}
|
|
874
|
+
/** Watch membership of one view ID without reacting to unrelated rows. */
|
|
875
|
+
watchViewHas(view, id, options) {
|
|
876
|
+
return this.viewSelectionHub(view, options).watch((state, emit) => state.subscribeItem(id, (item) => emit(item !== undefined)));
|
|
877
|
+
}
|
|
878
|
+
/** Watch live view cardinality without materializing its result array. */
|
|
879
|
+
watchViewSize(view, options) {
|
|
880
|
+
return this.viewSelectionHub(view, options).watch((state, emit) => state.subscribeSize(emit));
|
|
881
|
+
}
|
|
882
|
+
querySelectionHub(query, options) {
|
|
883
|
+
const cacheKey = queryCacheKey(query, options);
|
|
884
|
+
const existing = this.querySelectionHubs.get(cacheKey);
|
|
885
|
+
if (existing) {
|
|
886
|
+
return existing;
|
|
887
|
+
}
|
|
888
|
+
let hub;
|
|
889
|
+
hub = new LiveSelectionHub(() => this.watchQueryState(query, options), () => {
|
|
890
|
+
if (this.querySelectionHubs.get(cacheKey) === hub) {
|
|
891
|
+
this.querySelectionHubs.delete(cacheKey);
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
this.querySelectionHubs.set(cacheKey, hub);
|
|
895
|
+
return hub;
|
|
896
|
+
}
|
|
897
|
+
viewSelectionHub(view, options) {
|
|
898
|
+
const cacheKey = viewCacheKey(view, options);
|
|
899
|
+
const existing = this.viewSelectionHubs.get(cacheKey);
|
|
900
|
+
if (existing) {
|
|
901
|
+
return existing;
|
|
902
|
+
}
|
|
903
|
+
let hub;
|
|
904
|
+
hub = new LiveSelectionHub(() => this.watchViewState(view, options), () => {
|
|
905
|
+
if (this.viewSelectionHubs.get(cacheKey) === hub) {
|
|
906
|
+
this.viewSelectionHubs.delete(cacheKey);
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
this.viewSelectionHubs.set(cacheKey, hub);
|
|
910
|
+
return hub;
|
|
911
|
+
}
|
|
676
912
|
/**
|
|
677
913
|
* Start a live query with a mutable server-side window.
|
|
678
914
|
* Use `setWindow` to scroll without re-subscribing.
|
|
@@ -731,6 +967,38 @@ export class MykoClient {
|
|
|
731
967
|
setWindow: (window) => this.setQueryWindow(tx, window),
|
|
732
968
|
};
|
|
733
969
|
}
|
|
970
|
+
/** Start a live query whose subsequent pages use stable ID keysets. */
|
|
971
|
+
watchQueryCursor(query, limit, options) {
|
|
972
|
+
const watch = this.watchQueryWindowed(query, {
|
|
973
|
+
...options,
|
|
974
|
+
window: { offset: 0, limit },
|
|
975
|
+
});
|
|
976
|
+
let requested = { limit };
|
|
977
|
+
const pageInfo$ = combineLatest([watch.results$, watch.windowInfo$]).pipe(map(([items, info]) => {
|
|
978
|
+
const rows = items;
|
|
979
|
+
const startCursor = rows.at(0)?.id ?? null;
|
|
980
|
+
const endCursor = rows.at(-1)?.id ?? null;
|
|
981
|
+
return {
|
|
982
|
+
totalCount: info.totalCount,
|
|
983
|
+
startCursor,
|
|
984
|
+
endCursor,
|
|
985
|
+
hasPreviousPage: requested.before !== undefined || requested.after !== undefined,
|
|
986
|
+
hasNextPage: rows.length === requested.limit &&
|
|
987
|
+
(info.totalCount === null || rows.length < info.totalCount),
|
|
988
|
+
};
|
|
989
|
+
}), shareReplay({ bufferSize: 1, refCount: true }));
|
|
990
|
+
const setWindow = (window) => {
|
|
991
|
+
requested = window;
|
|
992
|
+
this.setQueryCursorWindow(watch.tx, window);
|
|
993
|
+
};
|
|
994
|
+
return {
|
|
995
|
+
tx: watch.tx,
|
|
996
|
+
results$: watch.results$,
|
|
997
|
+
pageInfo$,
|
|
998
|
+
setWindow,
|
|
999
|
+
firstPage: () => setWindow({ limit }),
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
734
1002
|
/** Start a live view with a mutable server-side window. */
|
|
735
1003
|
watchViewWindowed(view, options) {
|
|
736
1004
|
const [tx, responses$] = this.startView(view, options);
|
|
@@ -932,7 +1200,16 @@ export class MykoClient {
|
|
|
932
1200
|
this.scheduleEventBatchFlush();
|
|
933
1201
|
}
|
|
934
1202
|
/** Send a command and wait for response */
|
|
935
|
-
sendCommand(command) {
|
|
1203
|
+
sendCommand(command, options = {}) {
|
|
1204
|
+
if (options.timeoutMs !== undefined &&
|
|
1205
|
+
(!Number.isFinite(options.timeoutMs) || options.timeoutMs < 0)) {
|
|
1206
|
+
return Promise.reject(new RangeError('Command timeoutMs must be a finite non-negative number'));
|
|
1207
|
+
}
|
|
1208
|
+
if (options.signal?.aborted) {
|
|
1209
|
+
const error = new Error('Command aborted before it was sent');
|
|
1210
|
+
error.name = 'AbortError';
|
|
1211
|
+
return Promise.reject(error);
|
|
1212
|
+
}
|
|
936
1213
|
const tx = uuid();
|
|
937
1214
|
const wrappedCommand = {
|
|
938
1215
|
command: {
|
|
@@ -944,25 +1221,55 @@ export class MykoClient {
|
|
|
944
1221
|
commandId: command.commandId,
|
|
945
1222
|
};
|
|
946
1223
|
return new Promise((resolve, reject) => {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
}
|
|
953
|
-
const
|
|
954
|
-
.
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1224
|
+
let timeout;
|
|
1225
|
+
const onAbort = () => {
|
|
1226
|
+
const error = new Error('Command aborted');
|
|
1227
|
+
error.name = 'AbortError';
|
|
1228
|
+
fail(error);
|
|
1229
|
+
};
|
|
1230
|
+
const cleanup = (removeQueuedCommand = false) => {
|
|
1231
|
+
this.pendingCommands.delete(tx);
|
|
1232
|
+
if (timeout !== undefined)
|
|
1233
|
+
clearTimeout(timeout);
|
|
1234
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
1235
|
+
if (removeQueuedCommand && this.messageQueue.length > 0) {
|
|
1236
|
+
this.messageQueue = this.messageQueue.filter((message) => this.messageTx(message) !== tx);
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
const fail = (error) => {
|
|
1240
|
+
cleanup(true);
|
|
1241
|
+
reject(error);
|
|
962
1242
|
};
|
|
963
|
-
this.
|
|
1243
|
+
this.pendingCommands.set(tx, {
|
|
1244
|
+
resolve: (message) => {
|
|
1245
|
+
cleanup();
|
|
1246
|
+
resolve(message.data.response);
|
|
1247
|
+
},
|
|
1248
|
+
reject: fail,
|
|
1249
|
+
});
|
|
1250
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
1251
|
+
if (options.timeoutMs !== undefined) {
|
|
1252
|
+
timeout = setTimeout(() => {
|
|
1253
|
+
const error = new Error(`Command timed out after ${options.timeoutMs}ms`);
|
|
1254
|
+
error.name = 'TimeoutError';
|
|
1255
|
+
fail(error);
|
|
1256
|
+
}, options.timeoutMs);
|
|
1257
|
+
}
|
|
1258
|
+
try {
|
|
1259
|
+
this.send({ event: MykoEvent.Command, data: wrappedCommand });
|
|
1260
|
+
}
|
|
1261
|
+
catch (error) {
|
|
1262
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
1263
|
+
}
|
|
964
1264
|
});
|
|
965
1265
|
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Bind a generated `*Graph` descriptor into endpoint-scoped live state,
|
|
1268
|
+
* aggregate, and mutation helpers.
|
|
1269
|
+
*/
|
|
1270
|
+
graph(graph) {
|
|
1271
|
+
return bindGraph(this, graph);
|
|
1272
|
+
}
|
|
966
1273
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
967
1274
|
// Private: Socket Management
|
|
968
1275
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1009,6 +1316,7 @@ export class MykoClient {
|
|
|
1009
1316
|
return loopback.has(a) && loopback.has(b);
|
|
1010
1317
|
}
|
|
1011
1318
|
closeAllSockets() {
|
|
1319
|
+
this.failPendingCommands(new Error('Disconnected before command completion'));
|
|
1012
1320
|
for (const timer of this.reconnectTimers.values())
|
|
1013
1321
|
clearTimeout(timer);
|
|
1014
1322
|
this.reconnectTimers.clear();
|
|
@@ -1071,6 +1379,7 @@ export class MykoClient {
|
|
|
1071
1379
|
this.sockets.delete(address);
|
|
1072
1380
|
this.endpointSockets.delete(endpointKey);
|
|
1073
1381
|
if (this.currentServer === address) {
|
|
1382
|
+
this.failPendingCommands(new Error(`Connection closed before command completion: ${address}`));
|
|
1074
1383
|
const next = this.getFirstOpenSocket();
|
|
1075
1384
|
if (next) {
|
|
1076
1385
|
this.setCurrentServer(next.address, `failover from ${address} to ${next.address}`);
|
|
@@ -1216,9 +1525,15 @@ export class MykoClient {
|
|
|
1216
1525
|
this.reportResponses.next(message);
|
|
1217
1526
|
break;
|
|
1218
1527
|
case MykoEvent.CommandResponse:
|
|
1528
|
+
this.pendingCommands
|
|
1529
|
+
.get(message.data.tx)
|
|
1530
|
+
?.resolve(message);
|
|
1219
1531
|
this.commandResponses.next(message);
|
|
1220
1532
|
break;
|
|
1221
1533
|
case MykoEvent.CommandError:
|
|
1534
|
+
this.pendingCommands
|
|
1535
|
+
.get(message.data.tx)
|
|
1536
|
+
?.reject(new Error(message.data.message));
|
|
1222
1537
|
this.commandErrors.next(message);
|
|
1223
1538
|
break;
|
|
1224
1539
|
case MykoEvent.QueryError:
|
|
@@ -1291,9 +1606,16 @@ export class MykoClient {
|
|
|
1291
1606
|
}
|
|
1292
1607
|
this.sendNow(message);
|
|
1293
1608
|
}
|
|
1609
|
+
failPendingCommands(error) {
|
|
1610
|
+
for (const pending of [...this.pendingCommands.values()]) {
|
|
1611
|
+
pending.reject(error);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1294
1614
|
messageTx(message) {
|
|
1295
1615
|
const data = message.data;
|
|
1296
|
-
|
|
1616
|
+
if (typeof data?.tx === 'string')
|
|
1617
|
+
return data.tx;
|
|
1618
|
+
return typeof data?.command?.tx === 'string' ? data.command.tx : null;
|
|
1297
1619
|
}
|
|
1298
1620
|
sendNow(message) {
|
|
1299
1621
|
const event = message.event;
|
|
@@ -1347,11 +1669,18 @@ export class MykoClient {
|
|
|
1347
1669
|
return query;
|
|
1348
1670
|
}
|
|
1349
1671
|
resendSubscriptions() {
|
|
1350
|
-
for (const q of this.activeQueries
|
|
1672
|
+
for (const [tx, q] of this.activeQueries) {
|
|
1351
1673
|
this.send({
|
|
1352
1674
|
event: MykoEvent.Query,
|
|
1353
1675
|
data: this.withReconnectSequenceReset(q),
|
|
1354
1676
|
});
|
|
1677
|
+
const cursor = this.activeQueryCursorWindows.get(tx);
|
|
1678
|
+
if (cursor) {
|
|
1679
|
+
this.send({
|
|
1680
|
+
event: 'ws:m:query-cursor-window',
|
|
1681
|
+
data: { tx, window: cursor },
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1355
1684
|
}
|
|
1356
1685
|
for (const v of this.activeViews.values()) {
|
|
1357
1686
|
this.send({ event: MykoEvent.View, data: v });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AdjacencyPolicy.d.ts","sourceRoot":"","sources":["../../src/generated/AdjacencyPolicy.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,OAAO,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Direction.d.ts","sourceRoot":"","sources":["../../src/generated/Direction.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EdgeShapeKind.d.ts","sourceRoot":"","sources":["../../src/generated/EdgeShapeKind.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EndPosition.d.ts","sourceRoot":"","sources":["../../src/generated/EndPosition.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,GAAG,GAAG,GAAG,GAAG,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EndpointDeletePolicy.d.ts","sourceRoot":"","sources":["../../src/generated/EndpointDeletePolicy.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,wBAAwB,GAAG,gBAAgB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EntityRef.d.ts","sourceRoot":"","sources":["../../src/generated/EntityRef.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;CAAE,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AdjacencyPolicy } from "./AdjacencyPolicy";
|
|
2
|
+
import type { EndPosition } from "./EndPosition";
|
|
3
|
+
/**
|
|
4
|
+
* Observed endpoint-query work and an actionable projection recommendation.
|
|
5
|
+
*/
|
|
6
|
+
export type GraphPlanTelemetry = {
|
|
7
|
+
edgeType: string;
|
|
8
|
+
position: EndPosition;
|
|
9
|
+
policy: AdjacencyPolicy;
|
|
10
|
+
lookups: bigint;
|
|
11
|
+
scannedEdges: bigint;
|
|
12
|
+
returnedEdges: bigint;
|
|
13
|
+
recommendEager: boolean;
|
|
14
|
+
reason: string;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=GraphPlanTelemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GraphPlanTelemetry.d.ts","sourceRoot":"","sources":["../../src/generated/GraphPlanTelemetry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;CAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GraphSyncResult.d.ts","sourceRoot":"","sources":["../../src/generated/GraphSyncResult.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;CAAE,CAAC"}
|