@myko/core 6.1.0 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/client.d.ts +114 -2
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +354 -25
  4. package/dist/generated/AdjacencyPolicy.d.ts +2 -0
  5. package/dist/generated/AdjacencyPolicy.d.ts.map +1 -0
  6. package/dist/generated/AdjacencyPolicy.js +2 -0
  7. package/dist/generated/Direction.d.ts +2 -0
  8. package/dist/generated/Direction.d.ts.map +1 -0
  9. package/dist/generated/Direction.js +2 -0
  10. package/dist/generated/EdgeShapeKind.d.ts +2 -0
  11. package/dist/generated/EdgeShapeKind.d.ts.map +1 -0
  12. package/dist/generated/EdgeShapeKind.js +2 -0
  13. package/dist/generated/EndPosition.d.ts +2 -0
  14. package/dist/generated/EndPosition.d.ts.map +1 -0
  15. package/dist/generated/EndPosition.js +2 -0
  16. package/dist/generated/EndpointDeletePolicy.d.ts +2 -0
  17. package/dist/generated/EndpointDeletePolicy.d.ts.map +1 -0
  18. package/dist/generated/EndpointDeletePolicy.js +2 -0
  19. package/dist/generated/EntityRef.d.ts +8 -0
  20. package/dist/generated/EntityRef.d.ts.map +1 -0
  21. package/dist/generated/EntityRef.js +2 -0
  22. package/dist/generated/GraphPlanTelemetry.d.ts +16 -0
  23. package/dist/generated/GraphPlanTelemetry.d.ts.map +1 -0
  24. package/dist/generated/GraphPlanTelemetry.js +1 -0
  25. package/dist/generated/GraphSyncResult.d.ts +10 -0
  26. package/dist/generated/GraphSyncResult.d.ts.map +1 -0
  27. package/dist/generated/GraphSyncResult.js +2 -0
  28. package/dist/generated/MykoMessage.d.ts +4 -0
  29. package/dist/generated/MykoMessage.d.ts.map +1 -1
  30. package/dist/generated/PairPolicy.d.ts +2 -0
  31. package/dist/generated/PairPolicy.d.ts.map +1 -0
  32. package/dist/generated/PairPolicy.js +2 -0
  33. package/dist/generated/PairProjectionPolicy.d.ts +2 -0
  34. package/dist/generated/PairProjectionPolicy.d.ts.map +1 -0
  35. package/dist/generated/PairProjectionPolicy.js +2 -0
  36. package/dist/generated/QueryCursorWindow.d.ts +12 -0
  37. package/dist/generated/QueryCursorWindow.d.ts.map +1 -0
  38. package/dist/generated/QueryCursorWindow.js +2 -0
  39. package/dist/generated/QueryCursorWindowUpdate.d.ts +6 -0
  40. package/dist/generated/QueryCursorWindowUpdate.d.ts.map +1 -0
  41. package/dist/generated/QueryCursorWindowUpdate.js +1 -0
  42. package/dist/generated/SelfLoopPolicy.d.ts +2 -0
  43. package/dist/generated/SelfLoopPolicy.d.ts.map +1 -0
  44. package/dist/generated/SelfLoopPolicy.js +2 -0
  45. package/dist/generated/TraversalPath.d.ts +12 -0
  46. package/dist/generated/TraversalPath.d.ts.map +1 -0
  47. package/dist/generated/TraversalPath.js +1 -0
  48. package/dist/generated/TraversalResult.d.ts +7 -0
  49. package/dist/generated/TraversalResult.d.ts.map +1 -0
  50. package/dist/generated/TraversalResult.js +1 -0
  51. package/dist/generated/index.d.ts +35 -13
  52. package/dist/generated/index.d.ts.map +1 -1
  53. package/dist/generated/index.js +14 -11
  54. package/dist/graph-client.d.ts +128 -0
  55. package/dist/graph-client.d.ts.map +1 -0
  56. package/dist/graph-client.js +192 -0
  57. package/dist/index.d.ts +4 -2
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +2 -0
  60. package/dist/live-collection.d.ts +94 -0
  61. package/dist/live-collection.d.ts.map +1 -0
  62. package/dist/live-collection.js +302 -0
  63. package/package.json +5 -2
@@ -0,0 +1,192 @@
1
+ import { combineLatest, map } from 'rxjs';
2
+ import { LiveIndex } from './live-collection.js';
3
+ function descriptorSchema(graph) {
4
+ const schema = graph.$schema;
5
+ if (!schema || typeof schema !== 'object')
6
+ return null;
7
+ const candidate = schema;
8
+ if ((candidate.pairProjection !== 'intersectAdjacency' &&
9
+ candidate.pairProjection !== 'eager') ||
10
+ (candidate.aAdjacency !== 'demandDriven' &&
11
+ candidate.aAdjacency !== 'eager') ||
12
+ (candidate.bAdjacency !== 'demandDriven' &&
13
+ candidate.bAdjacency !== 'eager')) {
14
+ return null;
15
+ }
16
+ return candidate;
17
+ }
18
+ function endpointPlan(schema, position) {
19
+ if (!schema) {
20
+ return {
21
+ strategy: 'unknown',
22
+ initialization: 'unknown',
23
+ liveUpdates: 'unknown',
24
+ windowing: 'unknown',
25
+ reason: 'This graph descriptor predates generated schema metadata.',
26
+ };
27
+ }
28
+ const adjacency = position === 'a' ? schema.aAdjacency : schema.bAdjacency;
29
+ if (adjacency === 'eager') {
30
+ return {
31
+ strategy: 'eagerEndpoint',
32
+ initialization: 'indexLookup',
33
+ liveUpdates: 'routed',
34
+ windowing: 'pushdown',
35
+ reason: `Endpoint ${position.toUpperCase()} has an eager adjacency projection.`,
36
+ };
37
+ }
38
+ return {
39
+ strategy: 'demandDrivenScan',
40
+ initialization: 'canonicalScan',
41
+ liveUpdates: 'routed',
42
+ windowing: 'materialized',
43
+ reason: `Endpoint ${position.toUpperCase()} is demand-driven; initialization scans canonical edges.`,
44
+ };
45
+ }
46
+ function pairPlan(schema) {
47
+ if (!schema)
48
+ return endpointPlan(null, 'a');
49
+ if (schema.pairProjection === 'eager') {
50
+ return {
51
+ strategy: 'eagerPair',
52
+ initialization: 'indexLookup',
53
+ liveUpdates: 'routed',
54
+ windowing: 'pushdown',
55
+ reason: 'Exact endpoint pairs have an eager pair projection.',
56
+ };
57
+ }
58
+ if (schema.aAdjacency === 'eager' || schema.bAdjacency === 'eager') {
59
+ return {
60
+ strategy: 'adjacencyFilter',
61
+ initialization: 'indexLookup',
62
+ liveUpdates: 'routed',
63
+ windowing: 'materialized',
64
+ reason: 'Exact pairs filter the available eager endpoint adjacency.',
65
+ };
66
+ }
67
+ return {
68
+ strategy: 'demandDrivenScan',
69
+ initialization: 'canonicalScan',
70
+ liveUpdates: 'routed',
71
+ windowing: 'materialized',
72
+ reason: 'Exact pairs have no eager projection; initialization scans canonical edges.',
73
+ };
74
+ }
75
+ function factory(graph, key) {
76
+ const candidate = graph[key];
77
+ if (typeof candidate !== 'function') {
78
+ throw new TypeError(`Graph helper '${key}' is not available`);
79
+ }
80
+ return candidate;
81
+ }
82
+ /** Bind generated graph query/report/command factories to a client instance. */
83
+ export function bindGraph(client, graph) {
84
+ const schema = descriptorSchema(graph);
85
+ const plans = Object.freeze({
86
+ a: Object.freeze(endpointPlan(schema, 'a')),
87
+ b: Object.freeze(endpointPlan(schema, 'b')),
88
+ pair: Object.freeze(pairPlan(schema)),
89
+ });
90
+ const queryState = (key, args, options) => client.watchQueryState(factory(graph, key)(...args), options);
91
+ const reportState = (key, args) => client.watchReport(factory(graph, key)(...args));
92
+ const querySelection = (key, args, select, options) => client.watchQuerySelection(factory(graph, key)(...args), select, options);
93
+ const queryItem = (key, args, id, options) => client.watchQueryItem(factory(graph, key)(...args), id, options);
94
+ const queryHas = (key, args, id, options) => client.watchQueryHas(factory(graph, key)(...args), id, options);
95
+ const command = (key, args, options) => client.sendCommand(factory(graph, key)(...args), options);
96
+ const windowed = (key, args, window) => client.watchQueryWindowed(factory(graph, key)(...args), { window });
97
+ const cursor = (key, args, limit) => client.watchQueryCursor(factory(graph, key)(...args), limit);
98
+ const queryIndex = (key, args, keyOf, options) => {
99
+ const index = new LiveIndex(keyOf);
100
+ return queryState(key, args, options).pipe(map((state) => index.update(state)));
101
+ };
102
+ const endpoint = (edgeKey, exactEdgeKey, exactIdsKey, relatedKey, relatedName, countKey, traversalKey, syncKey, args, plan) => {
103
+ const exactEdgeQuery = (id) => exactEdgeKey && exactEdgeKey in graph
104
+ ? { key: exactEdgeKey, args: [...args, id] }
105
+ : { key: edgeKey, args };
106
+ const scope = {
107
+ plan,
108
+ edges: (options) => queryState(edgeKey, args, options),
109
+ selectEdges: (select, options) => querySelection(edgeKey, args, select, options),
110
+ edge: (id, options) => {
111
+ const exact = exactEdgeQuery(id);
112
+ return queryItem(exact.key, exact.args, id, options);
113
+ },
114
+ hasEdge: (id, options) => {
115
+ const exact = exactEdgeQuery(id);
116
+ return queryHas(exact.key, exact.args, id, options);
117
+ },
118
+ edgesWindowed: (window) => windowed(edgeKey, args, window),
119
+ edgesCursor: (limit) => cursor(edgeKey, args, limit),
120
+ edgesBy: (keyOf, options) => queryIndex(edgeKey, args, keyOf, options),
121
+ };
122
+ if (exactIdsKey && exactIdsKey in graph) {
123
+ scope.edgesByIds = (ids, options) => queryState(exactIdsKey, [...args, ids], options);
124
+ }
125
+ if (relatedKey in graph) {
126
+ const capitalizedName = relatedName === 'targets' ? 'Targets' : 'Sources';
127
+ const singularName = relatedName === 'targets' ? 'target' : 'source';
128
+ scope[relatedName] = (options) => queryState(relatedKey, args, options);
129
+ scope[`select${capitalizedName}`] = (select, options) => querySelection(relatedKey, args, select, options);
130
+ scope[singularName] = (id, options) => queryItem(relatedKey, args, id, options);
131
+ scope[`has${capitalizedName.slice(0, -1)}`] = (id, options) => queryHas(relatedKey, args, id, options);
132
+ scope[`${relatedName}Windowed`] = (window) => windowed(relatedKey, args, window);
133
+ scope.connections = (relatedId, options) => combineLatest([
134
+ queryState(edgeKey, args, options),
135
+ queryState(relatedKey, args, options),
136
+ ]).pipe(map(([edges, entities]) => edges.toArray().flatMap((edge) => {
137
+ const entity = entities.get(relatedId(edge));
138
+ return entity ? [{ edge, entity }] : [];
139
+ })));
140
+ }
141
+ if (countKey) {
142
+ scope.count = () => reportState(countKey, args);
143
+ }
144
+ if (traversalKey && traversalKey in graph) {
145
+ scope.traverse = (options) => reportState(traversalKey, [...args, options]);
146
+ }
147
+ if (syncKey && syncKey in graph) {
148
+ scope.sync = (edges, scopeOrOptions, maybeOptions) => {
149
+ const syncFactory = factory(graph, syncKey);
150
+ return syncFactory.length === 2
151
+ ? command(syncKey, [args[0], edges], scopeOrOptions)
152
+ : command(syncKey, [args[0], scopeOrOptions, edges], maybeOptions);
153
+ };
154
+ }
155
+ return scope;
156
+ };
157
+ const bound = {
158
+ schema,
159
+ from: (value) => endpoint('from', 'fromId', 'fromIds', 'targetsFrom', 'targets', 'countFrom', 'traverseFrom', 'syncFrom', [value], plans.a),
160
+ to: (value) => endpoint('to', 'toId', 'toIds', 'sourcesTo', 'sources', 'countTo', 'traverseTo', 'syncTo', [value], plans.b),
161
+ between: (a, b) => ({
162
+ plan: plans.pair,
163
+ edges: (options) => queryState('between', [a, b], options),
164
+ selectEdges: (select, options) => querySelection('between', [a, b], select, options),
165
+ edge: (id, options) => 'betweenId' in graph
166
+ ? queryItem('betweenId', [a, b, id], id, options)
167
+ : queryItem('between', [a, b], id, options),
168
+ hasEdge: (id, options) => 'betweenId' in graph
169
+ ? queryHas('betweenId', [a, b, id], id, options)
170
+ : queryHas('between', [a, b], id, options),
171
+ edgesWindowed: (window) => windowed('between', [a, b], window),
172
+ edgesCursor: (limit) => cursor('between', [a, b], limit),
173
+ count: () => reportState('countBetween', [a, b]),
174
+ exists: () => reportState('existsBetween', [a, b]),
175
+ ...('betweenIds' in graph
176
+ ? {
177
+ edgesByIds: (ids, options) => queryState('betweenIds', [a, b, ids], options),
178
+ }
179
+ : {}),
180
+ }),
181
+ connect: (edge, options) => command('connect', [edge], options),
182
+ connectMany: (edges, options) => command('connectMany', [edges], options),
183
+ ensure: (edge, options) => command('ensure', [edge], options),
184
+ disconnect: (id, options) => command('disconnect', [id], options),
185
+ disconnectMany: (ids, options) => command('disconnectMany', [ids], options),
186
+ };
187
+ if ('fromMany' in graph) {
188
+ bound.fromMany = (values) => endpoint('fromMany', null, null, 'targetsFromMany', 'targets', null, null, null, [values], plans.a);
189
+ bound.toMany = (values) => endpoint('toMany', null, null, 'sourcesToMany', 'sources', null, null, null, [values], plans.b);
190
+ }
191
+ return bound;
192
+ }
package/dist/index.d.ts CHANGED
@@ -4,8 +4,10 @@
4
4
  * Browser-compatible WebSocket client with RxJS Observables,
5
5
  * plus Rust-generated type bindings.
6
6
  */
7
- import type { ID, MEvent, WrappedItem, ItemStub, LogLevel } from './generated';
7
+ import type { ID, ItemStub, LogLevel, MEvent, WrappedItem } from './generated';
8
8
  export * from './generated';
9
+ export { type BoundGraph, bindGraph, type GraphDescriptorSchema, type GraphQueryPlan, type WindowedGraphQuery, } from './graph-client.js';
10
+ export { applyCollectionDiff, type CollectionChanges, type CollectionDiff, type Identified, LiveCollection, type LiveCollectionStatus, LiveIndex, type LiveIndexChanges, type MutableKeyedCollection, } from './live-collection.js';
9
11
  export type { LogLevel };
10
12
  /**
11
13
  * Get the item type name from an entity type or query class.
@@ -46,7 +48,7 @@ export declare const LOG_RANK: readonly ["ERROR", "WARN", "INFO", "DEBUG", "VERB
46
48
  * GetEventLog is now ServerEventLog in the Rust system.
47
49
  * @deprecated Use ServerEventLog from ./generated instead
48
50
  */
49
- export { ConnectionStatus, MykoClient, MykoProtocol, type ClientStats, type Command, type CommandResult, type MykoError, type MykoErrorEvent, type Query, type QueryDiff, type QueryItem, type QueryWatchOptions, type QueryWindow, type QueryWindowInfo, type QueryResult, type View, type ViewItem, type ViewResult, type Report, type ReportResult, } from './client.js';
51
+ export { type ClientStats, type Command, type CommandOptions, type CommandResult, ConnectionStatus, MykoClient, type MykoError, type MykoErrorEvent, MykoProtocol, type Query, type QueryDiff, type QueryItem, type QueryResult, type QuerySelectionOptions, type QueryWatchOptions, type QueryWindow, type QueryWindowInfo, type QueryCursorWindow, type QueryCursorPageInfo, type Report, type ReportResult, type View, type ViewItem, type ViewResult, } from './client.js';
50
52
  /**
51
53
  * Container for an event with associated metadata.
52
54
  * @deprecated Will be generated from Rust
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAI9E,cAAc,aAAa,CAAA;AAG3B,YAAY,EAAE,QAAQ,EAAE,CAAA;AAExB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,GACvD,MAAM,CAOR;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,OAAO,IAAI,WAAW,CAAA;AAEnD;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,CAAA;AAEhC;;;GAGG;AACH,eAAO,MAAM,QAAQ,wDAMX,CAAA;AAEV;;;GAGG;AAIH,OAAO,EACL,gBAAgB,EAChB,UAAU,EACV,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,IAAI,EACT,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,MAAM,EACX,KAAK,YAAY,GAClB,MAAM,aAAa,CAAA;AAMpB;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,EAAE,CAAA;IACN,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;GAGG;AACH,qBAAa,eAAe;IAC1B,MAAM,CAAC,QAAQ,CAAC,OAAO,qBAAoB;IAC3C,QAAQ,CAAC,OAAO,qBAAoB;IACpC,MAAM,CAAC,QAAQ,CAAC,aAAa,oBAAmB;IAChD,QAAQ,CAAC,aAAa,oBAAmB;IACzC,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAElE,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAG,MAAM,cAAc,EAAE,CAAA;gBAE1B,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM;CAG3D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAI9E,cAAc,aAAa,CAAA;AAC3B,OAAO,EACL,KAAK,UAAU,EACf,SAAS,EACT,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,kBAAkB,GACxB,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EACL,mBAAmB,EACnB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,cAAc,EACd,KAAK,oBAAoB,EACzB,SAAS,EACT,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,GAC5B,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,QAAQ,EAAE,CAAA;AAExB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,GACvD,MAAM,CAOR;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,OAAO,IAAI,WAAW,CAAA;AAEnD;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,CAAA;AAEhC;;;GAGG;AACH,eAAO,MAAM,QAAQ,wDAMX,CAAA;AAEV;;;GAGG;AAIH,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,gBAAgB,EAChB,UAAU,EACV,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,YAAY,EACZ,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,IAAI,EACT,KAAK,QAAQ,EACb,KAAK,UAAU,GAChB,MAAM,aAAa,CAAA;AAMpB;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,EAAE,CAAA;IACN,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;GAGG;AACH,qBAAa,eAAe;IAC1B,MAAM,CAAC,QAAQ,CAAC,OAAO,qBAAoB;IAC3C,QAAQ,CAAC,OAAO,qBAAoB;IACpC,MAAM,CAAC,QAAQ,CAAC,aAAa,oBAAmB;IAChD,QAAQ,CAAC,aAAa,oBAAmB;IACzC,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAElE,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAG,MAAM,cAAc,EAAE,CAAA;gBAE1B,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM;CAG3D"}
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@
7
7
  // Re-export all Rust-generated types from ./generated
8
8
  // This includes the ID type alias (ID = string)
9
9
  export * from './generated';
10
+ export { bindGraph, } from './graph-client.js';
11
+ export { applyCollectionDiff, LiveCollection, LiveIndex, } from './live-collection.js';
10
12
  /**
11
13
  * Get the item type name from an entity type or query class.
12
14
  *
@@ -0,0 +1,94 @@
1
+ export type Identified = {
2
+ id: string;
3
+ };
4
+ export type LiveCollectionStatus = 'loading' | 'live' | 'error';
5
+ export type CollectionDiff<T> = {
6
+ sequence: bigint;
7
+ deletes: readonly string[];
8
+ upserts: readonly T[];
9
+ };
10
+ export type CollectionChanges<T> = {
11
+ sequence: bigint | null;
12
+ reset: boolean;
13
+ deletes: readonly string[];
14
+ upserts: readonly T[];
15
+ };
16
+ export interface MutableKeyedCollection<T> {
17
+ clear(): void;
18
+ delete(id: string): boolean;
19
+ set(id: string, item: T): unknown;
20
+ }
21
+ /** Apply a Myko collection diff to any Map-compatible reactive collection. */
22
+ export declare function applyCollectionDiff<T extends Identified>(target: MutableKeyedCollection<T>, diff: CollectionDiff<T>): CollectionChanges<T>;
23
+ /**
24
+ * Stable, read-only keyed state for a live query or view.
25
+ *
26
+ * Diffs update the backing map in O(changes). Array materialization is lazy and
27
+ * memoized per revision, so row-oriented consumers never pay an O(result size)
28
+ * cost for an unrelated one-row update.
29
+ */
30
+ export declare class LiveCollection<T extends Identified> {
31
+ #private;
32
+ get items(): ReadonlyMap<string, T>;
33
+ get size(): number;
34
+ get status(): LiveCollectionStatus;
35
+ get resolved(): boolean;
36
+ get error(): Error | undefined;
37
+ get sequence(): bigint | null;
38
+ /** Monotonically increases for every diff or terminal error notification. */
39
+ get revision(): number;
40
+ /** The latest incremental change, suitable for framework-native adapters. */
41
+ get changes(): CollectionChanges<T>;
42
+ get(id: string): T | undefined;
43
+ has(id: string): boolean;
44
+ /**
45
+ * Subscribe to one keyed item in O(1).
46
+ *
47
+ * Only an initial authoritative snapshot, an upsert/delete of this ID, or a
48
+ * later sequence-zero reset invokes the listener. This is the low-level
49
+ * primitive used by fine-grained client and graph watchers.
50
+ */
51
+ subscribeItem(id: string, listener: (item: T | undefined) => void): () => void;
52
+ /** Subscribe to cardinality changes without observing unrelated row updates. */
53
+ subscribeSize(listener: (size: number) => void): () => void;
54
+ entries(): IterableIterator<[string, T]>;
55
+ /** Lazily materialize a stable array for the current revision. */
56
+ toArray(): readonly T[];
57
+ apply(diff: CollectionDiff<T>): this;
58
+ fail(error: unknown): this;
59
+ }
60
+ export type LiveIndexChanges<K> = {
61
+ reset: boolean;
62
+ keys: readonly K[];
63
+ };
64
+ /**
65
+ * Stable incremental secondary index over a `LiveCollection`.
66
+ *
67
+ * The first update groups the current snapshot. Consecutive source revisions
68
+ * then touch only deleted and upserted items, using cached prior keys to move
69
+ * an item between buckets without scanning the source collection. If a caller
70
+ * skips revisions, `update` safely rebuilds from the authoritative source.
71
+ */
72
+ export declare class LiveIndex<K, T extends Identified> {
73
+ #private;
74
+ constructor(keyOf: (item: T) => K);
75
+ get groups(): ReadonlyMap<K, ReadonlyMap<string, T>>;
76
+ get size(): number;
77
+ get status(): LiveCollectionStatus;
78
+ get resolved(): boolean;
79
+ get error(): Error | undefined;
80
+ get sequence(): bigint | null;
81
+ /** Increases once for every source revision incorporated by this index. */
82
+ get revision(): number;
83
+ /** Last incorporated `LiveCollection.revision`, or -1 before first update. */
84
+ get sourceRevision(): number;
85
+ /** Endpoint buckets affected by the latest incorporated source revision. */
86
+ get changes(): LiveIndexChanges<K>;
87
+ has(key: K): boolean;
88
+ /** A stable keyed bucket, or a shared immutable empty bucket. */
89
+ get(key: K): ReadonlyMap<string, T>;
90
+ /** Lazily materialize and memoize one bucket for the current revision. */
91
+ values(key: K): readonly T[];
92
+ update(source: LiveCollection<T>): this;
93
+ }
94
+ //# sourceMappingURL=live-collection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-collection.d.ts","sourceRoot":"","sources":["../src/live-collection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,302 @@
1
+ const EMPTY_CHANGES = {
2
+ sequence: null,
3
+ reset: false,
4
+ deletes: [],
5
+ upserts: [],
6
+ };
7
+ /** Apply a Myko collection diff to any Map-compatible reactive collection. */
8
+ export function applyCollectionDiff(target, diff) {
9
+ const reset = diff.sequence === 0n;
10
+ if (reset)
11
+ target.clear();
12
+ for (const id of diff.deletes)
13
+ target.delete(id);
14
+ for (const item of diff.upserts)
15
+ target.set(item.id, item);
16
+ return {
17
+ sequence: diff.sequence,
18
+ reset,
19
+ deletes: diff.deletes,
20
+ upserts: diff.upserts,
21
+ };
22
+ }
23
+ /**
24
+ * Stable, read-only keyed state for a live query or view.
25
+ *
26
+ * Diffs update the backing map in O(changes). Array materialization is lazy and
27
+ * memoized per revision, so row-oriented consumers never pay an O(result size)
28
+ * cost for an unrelated one-row update.
29
+ */
30
+ export class LiveCollection {
31
+ #items = new Map();
32
+ #itemListeners = new Map();
33
+ #sizeListeners = new Set();
34
+ #array;
35
+ #status = 'loading';
36
+ #error;
37
+ #sequence = null;
38
+ #revision = 0;
39
+ #changes = EMPTY_CHANGES;
40
+ get items() {
41
+ return this.#items;
42
+ }
43
+ get size() {
44
+ return this.#items.size;
45
+ }
46
+ get status() {
47
+ return this.#status;
48
+ }
49
+ get resolved() {
50
+ return this.#sequence !== null;
51
+ }
52
+ get error() {
53
+ return this.#error;
54
+ }
55
+ get sequence() {
56
+ return this.#sequence;
57
+ }
58
+ /** Monotonically increases for every diff or terminal error notification. */
59
+ get revision() {
60
+ return this.#revision;
61
+ }
62
+ /** The latest incremental change, suitable for framework-native adapters. */
63
+ get changes() {
64
+ return this.#changes;
65
+ }
66
+ get(id) {
67
+ return this.#items.get(id);
68
+ }
69
+ has(id) {
70
+ return this.#items.has(id);
71
+ }
72
+ /**
73
+ * Subscribe to one keyed item in O(1).
74
+ *
75
+ * Only an initial authoritative snapshot, an upsert/delete of this ID, or a
76
+ * later sequence-zero reset invokes the listener. This is the low-level
77
+ * primitive used by fine-grained client and graph watchers.
78
+ */
79
+ subscribeItem(id, listener) {
80
+ let listeners = this.#itemListeners.get(id);
81
+ if (!listeners) {
82
+ listeners = new Set();
83
+ this.#itemListeners.set(id, listeners);
84
+ }
85
+ listeners.add(listener);
86
+ if (this.resolved)
87
+ listener(this.#items.get(id));
88
+ return () => {
89
+ const current = this.#itemListeners.get(id);
90
+ current?.delete(listener);
91
+ if (current?.size === 0)
92
+ this.#itemListeners.delete(id);
93
+ };
94
+ }
95
+ /** Subscribe to cardinality changes without observing unrelated row updates. */
96
+ subscribeSize(listener) {
97
+ this.#sizeListeners.add(listener);
98
+ if (this.resolved)
99
+ listener(this.#items.size);
100
+ return () => {
101
+ this.#sizeListeners.delete(listener);
102
+ };
103
+ }
104
+ entries() {
105
+ return this.#items.entries();
106
+ }
107
+ /** Lazily materialize a stable array for the current revision. */
108
+ toArray() {
109
+ this.#array ??= Array.from(this.#items.values());
110
+ return this.#array;
111
+ }
112
+ apply(diff) {
113
+ const wasResolved = this.resolved;
114
+ const previousSize = this.#items.size;
115
+ this.#changes = applyCollectionDiff(this.#items, diff);
116
+ this.#sequence = diff.sequence;
117
+ this.#status = 'live';
118
+ this.#error = undefined;
119
+ this.#revision += 1;
120
+ this.#array = undefined;
121
+ if (diff.sequence === 0n && this.#itemListeners.size > 0) {
122
+ for (const [id, listeners] of this.#itemListeners) {
123
+ const item = this.#items.get(id);
124
+ for (const listener of listeners)
125
+ listener(item);
126
+ }
127
+ }
128
+ else if (this.#itemListeners.size > 0) {
129
+ const changedIds = new Set(diff.deletes);
130
+ for (const item of diff.upserts)
131
+ changedIds.add(item.id);
132
+ for (const id of changedIds) {
133
+ const listeners = this.#itemListeners.get(id);
134
+ if (!listeners)
135
+ continue;
136
+ const item = this.#items.get(id);
137
+ for (const listener of listeners)
138
+ listener(item);
139
+ }
140
+ }
141
+ if (this.#sizeListeners.size > 0 &&
142
+ (!wasResolved || previousSize !== this.#items.size)) {
143
+ for (const listener of this.#sizeListeners)
144
+ listener(this.#items.size);
145
+ }
146
+ return this;
147
+ }
148
+ fail(error) {
149
+ this.#status = 'error';
150
+ this.#error = error instanceof Error ? error : new Error(String(error));
151
+ this.#revision += 1;
152
+ this.#changes = EMPTY_CHANGES;
153
+ return this;
154
+ }
155
+ }
156
+ const EMPTY_GROUP = new Map();
157
+ /**
158
+ * Stable incremental secondary index over a `LiveCollection`.
159
+ *
160
+ * The first update groups the current snapshot. Consecutive source revisions
161
+ * then touch only deleted and upserted items, using cached prior keys to move
162
+ * an item between buckets without scanning the source collection. If a caller
163
+ * skips revisions, `update` safely rebuilds from the authoritative source.
164
+ */
165
+ export class LiveIndex {
166
+ #keyOf;
167
+ #groups = new Map();
168
+ #itemKeys = new Map();
169
+ #arrays = new Map();
170
+ #source;
171
+ #sourceRevision = -1;
172
+ #revision = 0;
173
+ #changes = { reset: false, keys: [] };
174
+ constructor(keyOf) {
175
+ this.#keyOf = keyOf;
176
+ }
177
+ get groups() {
178
+ return this.#groups;
179
+ }
180
+ get size() {
181
+ return this.#groups.size;
182
+ }
183
+ get status() {
184
+ return this.#source?.status ?? 'loading';
185
+ }
186
+ get resolved() {
187
+ return this.#source?.resolved ?? false;
188
+ }
189
+ get error() {
190
+ return this.#source?.error;
191
+ }
192
+ get sequence() {
193
+ return this.#source?.sequence ?? null;
194
+ }
195
+ /** Increases once for every source revision incorporated by this index. */
196
+ get revision() {
197
+ return this.#revision;
198
+ }
199
+ /** Last incorporated `LiveCollection.revision`, or -1 before first update. */
200
+ get sourceRevision() {
201
+ return this.#sourceRevision;
202
+ }
203
+ /** Endpoint buckets affected by the latest incorporated source revision. */
204
+ get changes() {
205
+ return this.#changes;
206
+ }
207
+ has(key) {
208
+ return this.#groups.has(key);
209
+ }
210
+ /** A stable keyed bucket, or a shared immutable empty bucket. */
211
+ get(key) {
212
+ return this.#groups.get(key) ?? EMPTY_GROUP;
213
+ }
214
+ /** Lazily materialize and memoize one bucket for the current revision. */
215
+ values(key) {
216
+ const cached = this.#arrays.get(key);
217
+ if (cached)
218
+ return cached;
219
+ const values = Array.from(this.get(key).values());
220
+ this.#arrays.set(key, values);
221
+ return values;
222
+ }
223
+ update(source) {
224
+ if (source.revision === this.#sourceRevision)
225
+ return this;
226
+ const changedKeys = new Set();
227
+ const consecutive = source.revision === this.#sourceRevision + 1;
228
+ if (this.#sourceRevision < 0 || !consecutive || source.changes.reset) {
229
+ for (const key of this.#groups.keys())
230
+ changedKeys.add(key);
231
+ this.#rebuild(source.items.values());
232
+ for (const key of this.#groups.keys())
233
+ changedKeys.add(key);
234
+ this.#changes = { reset: true, keys: Array.from(changedKeys) };
235
+ }
236
+ else {
237
+ for (const id of source.changes.deletes)
238
+ this.#remove(id, changedKeys);
239
+ for (const item of source.changes.upserts)
240
+ this.#upsert(item, changedKeys);
241
+ this.#changes = { reset: false, keys: Array.from(changedKeys) };
242
+ }
243
+ this.#source = source;
244
+ this.#sourceRevision = source.revision;
245
+ this.#revision += 1;
246
+ return this;
247
+ }
248
+ #rebuild(items) {
249
+ for (const group of this.#groups.values())
250
+ group.clear();
251
+ this.#groups.clear();
252
+ this.#itemKeys.clear();
253
+ this.#arrays.clear();
254
+ for (const item of items)
255
+ this.#insert(item);
256
+ }
257
+ #insert(item) {
258
+ const key = this.#keyOf(item);
259
+ this.#insertAt(key, item);
260
+ }
261
+ #insertAt(key, item) {
262
+ let group = this.#groups.get(key);
263
+ if (!group) {
264
+ group = new Map();
265
+ this.#groups.set(key, group);
266
+ }
267
+ group.set(item.id, item);
268
+ this.#itemKeys.set(item.id, key);
269
+ this.#arrays.delete(key);
270
+ }
271
+ #upsert(item, changedKeys) {
272
+ const key = this.#keyOf(item);
273
+ if (this.#itemKeys.has(item.id)) {
274
+ const previousKey = this.#itemKeys.get(item.id);
275
+ const previousGroup = this.#groups.get(previousKey);
276
+ // Comparing the resolved buckets uses the Map's SameValueZero key
277
+ // semantics and preserves a stable bucket when membership is unchanged.
278
+ if (previousGroup && previousGroup === this.#groups.get(key)) {
279
+ previousGroup.set(item.id, item);
280
+ this.#itemKeys.set(item.id, key);
281
+ this.#arrays.delete(previousKey);
282
+ changedKeys.add(previousKey);
283
+ return;
284
+ }
285
+ this.#remove(item.id, changedKeys);
286
+ }
287
+ this.#insertAt(key, item);
288
+ changedKeys.add(key);
289
+ }
290
+ #remove(id, changedKeys) {
291
+ if (!this.#itemKeys.has(id))
292
+ return;
293
+ const key = this.#itemKeys.get(id);
294
+ const group = this.#groups.get(key);
295
+ group?.delete(id);
296
+ this.#itemKeys.delete(id);
297
+ this.#arrays.delete(key);
298
+ changedKeys?.add(key);
299
+ if (group?.size === 0)
300
+ this.#groups.delete(key);
301
+ }
302
+ }
package/package.json CHANGED
@@ -37,11 +37,14 @@
37
37
  "url": "https://github.com/ignition-is-go/myko"
38
38
  },
39
39
  "scripts": {
40
+ "bench:command-routing": "bun benches/command-routing.ts",
41
+ "bench:live-collection": "bun benches/live-collection.ts",
42
+ "bench:selection": "bun benches/selection-routing.ts",
40
43
  "build": "tsc",
41
44
  "check": "tsc --noEmit --skipLibCheck",
42
- "test": "echo 'no tests yet'"
45
+ "test": "bun test tests"
43
46
  },
44
47
  "type": "module",
45
48
  "types": "./dist/index.d.ts",
46
- "version": "6.1.0"
49
+ "version": "6.3.0"
47
50
  }