@gleanql/client 0.1.14 → 0.1.15
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/index.d.mts +14 -0
- package/dist/index.mjs +86 -20
- package/package.json +2 -2
- package/src/integration.ts +29 -18
- package/src/proxy.ts +91 -9
- package/src/runtime.ts +40 -0
package/dist/index.d.mts
CHANGED
|
@@ -283,6 +283,14 @@ declare class GraphRuntime {
|
|
|
283
283
|
* `reportMiss` (they aren't "unexpected" misses).
|
|
284
284
|
*/
|
|
285
285
|
resolveRoot(key: string, exec: () => Promise<Record<string, FieldValue>>): Record<string, FieldValue>;
|
|
286
|
+
/**
|
|
287
|
+
* Async twin of {@link resolveRoot} for non-React callers (server handlers,
|
|
288
|
+
* proxy): run `exec` once per `key` and RESOLVE to the seeded root refs instead
|
|
289
|
+
* of throwing a Suspense promise. Shares the `resolvedRoots` cache and the
|
|
290
|
+
* `pending` map with `resolveRoot`, so an `await` and a concurrent sync (thrown)
|
|
291
|
+
* read of the same root+args dedupe to a single in-flight fetch.
|
|
292
|
+
*/
|
|
293
|
+
resolveRootAsync(key: string, exec: () => Promise<Record<string, FieldValue>>): Promise<Record<string, FieldValue>>;
|
|
286
294
|
/** Seed a record's fields (e.g. from the compiled operation result). */
|
|
287
295
|
seed(ref: GraphRef, fields: Readonly<Record<string, FieldValue>>): void;
|
|
288
296
|
/** Normalize a full operation result into the cache; returns root refs. */
|
|
@@ -498,6 +506,12 @@ interface BindGraphOptions {
|
|
|
498
506
|
* the integration over `runtime.resolveRoot` + `resolveDeferredRoot`.
|
|
499
507
|
*/
|
|
500
508
|
readonly resolveDeferredRoot?: (rootField: string, args: Record<string, unknown> | undefined) => FieldValue;
|
|
509
|
+
/**
|
|
510
|
+
* Async twin of {@link resolveDeferredRoot}: resolve the seeded value without
|
|
511
|
+
* throwing, so a deferred root can be `await`ed in a non-React server handler
|
|
512
|
+
* (`const o = await glean.order({ id })`). Wired over `runtime.resolveRootAsync`.
|
|
513
|
+
*/
|
|
514
|
+
readonly resolveDeferredRootAsync?: (rootField: string, args: Record<string, unknown> | undefined) => Promise<FieldValue>;
|
|
501
515
|
}
|
|
502
516
|
declare function bindGraph(options: BindGraphOptions): BoundGraph;
|
|
503
517
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -510,6 +510,37 @@ var GraphRuntime = class GraphRuntime {
|
|
|
510
510
|
});
|
|
511
511
|
throw entry.promise;
|
|
512
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* Async twin of {@link resolveRoot} for non-React callers (server handlers,
|
|
515
|
+
* proxy): run `exec` once per `key` and RESOLVE to the seeded root refs instead
|
|
516
|
+
* of throwing a Suspense promise. Shares the `resolvedRoots` cache and the
|
|
517
|
+
* `pending` map with `resolveRoot`, so an `await` and a concurrent sync (thrown)
|
|
518
|
+
* read of the same root+args dedupe to a single in-flight fetch.
|
|
519
|
+
*/
|
|
520
|
+
async resolveRootAsync(key, exec) {
|
|
521
|
+
const done = this.resolvedRoots.get(key);
|
|
522
|
+
if (done) return done;
|
|
523
|
+
const pkey = `@root:${key}`;
|
|
524
|
+
const existing = this.pending.get(pkey);
|
|
525
|
+
if (existing) {
|
|
526
|
+
await existing.promise;
|
|
527
|
+
return this.resolvedRoots.get(key) ?? {};
|
|
528
|
+
}
|
|
529
|
+
const entry = this.makeDeferred();
|
|
530
|
+
entry.promise.catch(() => {});
|
|
531
|
+
this.pending.set(pkey, entry);
|
|
532
|
+
try {
|
|
533
|
+
const roots = await exec();
|
|
534
|
+
this.resolvedRoots.set(key, roots);
|
|
535
|
+
this.pending.delete(pkey);
|
|
536
|
+
entry.resolve();
|
|
537
|
+
return roots;
|
|
538
|
+
} catch (error) {
|
|
539
|
+
this.pending.delete(pkey);
|
|
540
|
+
entry.reject(error);
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
513
544
|
/** Seed a record's fields (e.g. from the compiled operation result). */
|
|
514
545
|
seed(ref, fields) {
|
|
515
546
|
this.cache.merge(ref, fields);
|
|
@@ -750,6 +781,7 @@ const handler = {
|
|
|
750
781
|
type: state.type
|
|
751
782
|
};
|
|
752
783
|
if (typeof prop === "symbol") return void 0;
|
|
784
|
+
if (prop === "then") return void 0;
|
|
753
785
|
if (prop === "__typename") return state.ref.__typename ?? readField(state, "__typename");
|
|
754
786
|
const fieldName = prop;
|
|
755
787
|
const fieldDef = state.binding.schema.getField(state.type, fieldName);
|
|
@@ -804,9 +836,8 @@ function bindGraph(options) {
|
|
|
804
836
|
...args ? { args } : {}
|
|
805
837
|
}];
|
|
806
838
|
if (options.deferredRoots?.has(fieldName) && options.resolveDeferredRoot) {
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
return wrap(binding, seededDeferred, fieldDef.type, trail);
|
|
839
|
+
const materialize = (seeded) => fieldDef.list ? (Array.isArray(seeded) ? seeded : []).map((item) => wrap(binding, item, fieldDef.type, trail)) : wrap(binding, seeded, fieldDef.type, trail);
|
|
840
|
+
return makeDeferredRootValue(fieldName, args, fieldDef.list ?? false, materialize, options.resolveDeferredRoot, options.resolveDeferredRootAsync);
|
|
810
841
|
}
|
|
811
842
|
const seeded = (typeof options.roots === "function" ? options.roots() : options.roots)?.[fieldName];
|
|
812
843
|
if (fieldDef.list) return (Array.isArray(seeded) ? seeded : []).map((item) => wrap(binding, item, fieldDef.type, trail));
|
|
@@ -814,6 +845,40 @@ function bindGraph(options) {
|
|
|
814
845
|
};
|
|
815
846
|
return graph;
|
|
816
847
|
}
|
|
848
|
+
/**
|
|
849
|
+
* A deferred ("two-sweep") root read is BOTH awaitable and directly readable, so
|
|
850
|
+
* the same call works in a React render and a plain server handler:
|
|
851
|
+
* - `await glean.order({ id })` → async-resolve, no Suspense (server handlers)
|
|
852
|
+
* - `glean.order({ id }).name` → sync-resolve, suspends until seeded (React)
|
|
853
|
+
*
|
|
854
|
+
* It's a proxy over an EMPTY array (list root) or object (singular root): `.then`
|
|
855
|
+
* drives the async executor (kept in the get-trap only, never an own property, so
|
|
856
|
+
* it is awaitable without leaking into enumeration/spread/JSON), and every other
|
|
857
|
+
* access sync-resolves (throwing a Suspense promise until the fetch+seed completes)
|
|
858
|
+
* then delegates to the materialized value. The array target keeps `Array.isArray`
|
|
859
|
+
* true for a list root, matching a non-deferred list read; the materialized value
|
|
860
|
+
* is memoized so repeated sync reads on the same returned value keep stable element
|
|
861
|
+
* identity. When no async executor is wired `.then` is absent, so the value is a
|
|
862
|
+
* plain (non-thenable) sync read — unchanged React behavior.
|
|
863
|
+
*/
|
|
864
|
+
function makeDeferredRootValue(rootField, args, isList, materialize, syncResolve, asyncResolve) {
|
|
865
|
+
const then = asyncResolve ? (onFulfilled, onRejected) => asyncResolve(rootField, args).then((seeded) => materialize(seeded)).then(onFulfilled, onRejected) : void 0;
|
|
866
|
+
let ready = false;
|
|
867
|
+
let value;
|
|
868
|
+
const syncValue = () => {
|
|
869
|
+
if (!ready) {
|
|
870
|
+
value = materialize(syncResolve(rootField, args));
|
|
871
|
+
ready = true;
|
|
872
|
+
}
|
|
873
|
+
return value;
|
|
874
|
+
};
|
|
875
|
+
return new Proxy(isList ? [] : {}, { get(_t, prop) {
|
|
876
|
+
if (prop === "then") return then;
|
|
877
|
+
const v = syncValue();
|
|
878
|
+
const got = v[prop];
|
|
879
|
+
return typeof got === "function" ? got.bind(v) : got;
|
|
880
|
+
} });
|
|
881
|
+
}
|
|
817
882
|
//#endregion
|
|
818
883
|
//#region src/cache-resolve.ts
|
|
819
884
|
/**
|
|
@@ -1463,22 +1528,22 @@ function createGraphIntegration(options) {
|
|
|
1463
1528
|
});
|
|
1464
1529
|
({variables, roots, errors} = r);
|
|
1465
1530
|
}
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1531
|
+
const deferredKey = (rootField, args) => `${rootField}(${canonicalArgs(toArgMap(args ?? {}))})`;
|
|
1532
|
+
const deferredExec = (rootField, args) => async () => {
|
|
1533
|
+
const res = await resolveDeferredRoot({
|
|
1534
|
+
op: operation,
|
|
1535
|
+
rootField,
|
|
1536
|
+
args: args ?? {},
|
|
1537
|
+
schema: options.schema,
|
|
1538
|
+
adapter: options.adapter,
|
|
1539
|
+
runtime,
|
|
1540
|
+
context: requestContext
|
|
1541
|
+
});
|
|
1542
|
+
if (!res.ok && res.error) throw new Error(res.error);
|
|
1543
|
+
return res.roots ?? {};
|
|
1544
|
+
};
|
|
1545
|
+
const resolveDeferred = operation.deferred ? (rootField, args) => runtime.resolveRoot(deferredKey(rootField, args), deferredExec(rootField, args))[rootField] : void 0;
|
|
1546
|
+
const resolveDeferredAsync = operation.deferred ? async (rootField, args) => (await runtime.resolveRootAsync(deferredKey(rootField, args), deferredExec(rootField, args)))[rootField] : void 0;
|
|
1482
1547
|
const active = {
|
|
1483
1548
|
runtime,
|
|
1484
1549
|
graph: bindGraph({
|
|
@@ -1486,7 +1551,8 @@ function createGraphIntegration(options) {
|
|
|
1486
1551
|
getRuntime: () => runtime,
|
|
1487
1552
|
roots,
|
|
1488
1553
|
...deferredRoots ? { deferredRoots } : {},
|
|
1489
|
-
...resolveDeferred ? { resolveDeferredRoot: resolveDeferred } : {}
|
|
1554
|
+
...resolveDeferred ? { resolveDeferredRoot: resolveDeferred } : {},
|
|
1555
|
+
...resolveDeferredAsync ? { resolveDeferredRootAsync: resolveDeferredAsync } : {}
|
|
1490
1556
|
}),
|
|
1491
1557
|
mutate: createMutator({
|
|
1492
1558
|
operations: options.operations,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gleanql/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Glean's runtime: normalized cache, fine-grained reactivity, mutations, subscriptions and React hooks",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alexander Liljengard",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@gleanql/core": "0.1.
|
|
29
|
+
"@gleanql/core": "0.1.15"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"react": ">=18"
|
package/src/integration.ts
CHANGED
|
@@ -166,25 +166,35 @@ export function createGraphIntegration<Ctx extends Record<string, unknown> = Rec
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
// The deferred-root executor: fetch a render-time root with its call-site
|
|
169
|
-
// args
|
|
169
|
+
// args, seed the cache, and resolve to the seeded ref(s). `runDeferredRoot` is
|
|
170
|
+
// the shared async fetch; the SYNC form (`resolveRoot`) throws a Suspense
|
|
171
|
+
// promise for React renders, the ASYNC form (`resolveRootAsync`) resolves for
|
|
172
|
+
// non-React callers (`await glean.order({ id })` in a server handler). Both
|
|
173
|
+
// dedupe on the same root+args key.
|
|
174
|
+
const deferredKey = (rootField: string, args: Record<string, unknown> | undefined) =>
|
|
175
|
+
`${rootField}(${canonicalArgs(toArgMap(args ?? {}))})`;
|
|
176
|
+
const deferredExec =
|
|
177
|
+
(rootField: string, args: Record<string, unknown> | undefined) => async (): Promise<Record<string, FieldValue>> => {
|
|
178
|
+
const res = await runDeferredRoot({
|
|
179
|
+
op: operation,
|
|
180
|
+
rootField,
|
|
181
|
+
args: args ?? {},
|
|
182
|
+
schema: options.schema,
|
|
183
|
+
adapter: options.adapter,
|
|
184
|
+
runtime,
|
|
185
|
+
context: requestContext,
|
|
186
|
+
});
|
|
187
|
+
if (!res.ok && res.error) throw new Error(res.error);
|
|
188
|
+
return res.roots ?? {};
|
|
189
|
+
};
|
|
190
|
+
|
|
170
191
|
const resolveDeferred = operation.deferred
|
|
171
|
-
? (rootField: string, args: Record<string, unknown> | undefined): FieldValue =>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
args: args ?? {},
|
|
178
|
-
schema: options.schema,
|
|
179
|
-
adapter: options.adapter,
|
|
180
|
-
runtime,
|
|
181
|
-
context: requestContext,
|
|
182
|
-
});
|
|
183
|
-
if (!res.ok && res.error) throw new Error(res.error);
|
|
184
|
-
return res.roots ?? {};
|
|
185
|
-
});
|
|
186
|
-
return seededRoots[rootField];
|
|
187
|
-
}
|
|
192
|
+
? (rootField: string, args: Record<string, unknown> | undefined): FieldValue =>
|
|
193
|
+
runtime.resolveRoot(deferredKey(rootField, args), deferredExec(rootField, args))[rootField]
|
|
194
|
+
: undefined;
|
|
195
|
+
const resolveDeferredAsync = operation.deferred
|
|
196
|
+
? async (rootField: string, args: Record<string, unknown> | undefined): Promise<FieldValue> =>
|
|
197
|
+
(await runtime.resolveRootAsync(deferredKey(rootField, args), deferredExec(rootField, args)))[rootField]
|
|
188
198
|
: undefined;
|
|
189
199
|
|
|
190
200
|
const graph = bindGraph({
|
|
@@ -193,6 +203,7 @@ export function createGraphIntegration<Ctx extends Record<string, unknown> = Rec
|
|
|
193
203
|
roots,
|
|
194
204
|
...(deferredRoots ? { deferredRoots } : {}),
|
|
195
205
|
...(resolveDeferred ? { resolveDeferredRoot: resolveDeferred } : {}),
|
|
206
|
+
...(resolveDeferredAsync ? { resolveDeferredRootAsync: resolveDeferredAsync } : {}),
|
|
196
207
|
});
|
|
197
208
|
const mutate = createMutator({ operations: options.operations, adapter: options.adapter, runtime, context: requestContext });
|
|
198
209
|
|
package/src/proxy.ts
CHANGED
|
@@ -175,6 +175,11 @@ const handler: ProxyHandler<{ state: ProxyState }> = {
|
|
|
175
175
|
return { ref: state.ref, type: state.type } satisfies GraphSelection;
|
|
176
176
|
}
|
|
177
177
|
if (typeof prop === "symbol") return undefined;
|
|
178
|
+
// Never look like a thenable: a graph proxy can be the resolved value of
|
|
179
|
+
// `await glean.order({ id })`, and Promise resolution probes `.then` — reading
|
|
180
|
+
// it as a (missing) field would suspend/throw mid-resolution. GraphQL has no
|
|
181
|
+
// `then` field in practice, so returning undefined is safe.
|
|
182
|
+
if (prop === "then") return undefined;
|
|
178
183
|
if (prop === "__typename") {
|
|
179
184
|
// Identity field: prefer the ref's own typename, fall back to a cache read.
|
|
180
185
|
return state.ref.__typename ?? readField(state, "__typename");
|
|
@@ -265,6 +270,15 @@ export interface BindGraphOptions {
|
|
|
265
270
|
* the integration over `runtime.resolveRoot` + `resolveDeferredRoot`.
|
|
266
271
|
*/
|
|
267
272
|
readonly resolveDeferredRoot?: (rootField: string, args: Record<string, unknown> | undefined) => FieldValue;
|
|
273
|
+
/**
|
|
274
|
+
* Async twin of {@link resolveDeferredRoot}: resolve the seeded value without
|
|
275
|
+
* throwing, so a deferred root can be `await`ed in a non-React server handler
|
|
276
|
+
* (`const o = await glean.order({ id })`). Wired over `runtime.resolveRootAsync`.
|
|
277
|
+
*/
|
|
278
|
+
readonly resolveDeferredRootAsync?: (
|
|
279
|
+
rootField: string,
|
|
280
|
+
args: Record<string, unknown> | undefined,
|
|
281
|
+
) => Promise<FieldValue>;
|
|
268
282
|
}
|
|
269
283
|
|
|
270
284
|
export function bindGraph(options: BindGraphOptions): BoundGraph {
|
|
@@ -282,16 +296,24 @@ export function bindGraph(options: BindGraphOptions): BoundGraph {
|
|
|
282
296
|
const trail: PathStep[] = [{ name: fieldName, ...(args ? { args } : {}) }];
|
|
283
297
|
|
|
284
298
|
// Deferred ("two-sweep") root: args are only known at the render call-site,
|
|
285
|
-
// so execute on demand
|
|
286
|
-
//
|
|
287
|
-
//
|
|
299
|
+
// so execute on demand instead of reading a preloaded root. The returned
|
|
300
|
+
// value is ISOMORPHIC — `await glean.nodes({ ids })` resolves it (server
|
|
301
|
+
// handlers), while `glean.nodes({ ids }).map(...)` reads it synchronously via
|
|
302
|
+
// Suspense (React). This also replaces the silent empty-array fallback below:
|
|
303
|
+
// a deferred list root fetches, it does not yield [].
|
|
288
304
|
if (options.deferredRoots?.has(fieldName) && options.resolveDeferredRoot) {
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
305
|
+
const materialize = (seeded: FieldValue): unknown =>
|
|
306
|
+
fieldDef.list
|
|
307
|
+
? (Array.isArray(seeded) ? seeded : []).map((item) => wrap(binding, item, fieldDef.type, trail))
|
|
308
|
+
: wrap(binding, seeded, fieldDef.type, trail);
|
|
309
|
+
return makeDeferredRootValue(
|
|
310
|
+
fieldName,
|
|
311
|
+
args,
|
|
312
|
+
fieldDef.list ?? false,
|
|
313
|
+
materialize,
|
|
314
|
+
options.resolveDeferredRoot,
|
|
315
|
+
options.resolveDeferredRootAsync,
|
|
316
|
+
);
|
|
295
317
|
}
|
|
296
318
|
|
|
297
319
|
const rootsNow =
|
|
@@ -313,3 +335,63 @@ export function bindGraph(options: BindGraphOptions): BoundGraph {
|
|
|
313
335
|
}
|
|
314
336
|
return graph as BoundGraph;
|
|
315
337
|
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* A deferred ("two-sweep") root read is BOTH awaitable and directly readable, so
|
|
341
|
+
* the same call works in a React render and a plain server handler:
|
|
342
|
+
* - `await glean.order({ id })` → async-resolve, no Suspense (server handlers)
|
|
343
|
+
* - `glean.order({ id }).name` → sync-resolve, suspends until seeded (React)
|
|
344
|
+
*
|
|
345
|
+
* It's a proxy over an EMPTY array (list root) or object (singular root): `.then`
|
|
346
|
+
* drives the async executor (kept in the get-trap only, never an own property, so
|
|
347
|
+
* it is awaitable without leaking into enumeration/spread/JSON), and every other
|
|
348
|
+
* access sync-resolves (throwing a Suspense promise until the fetch+seed completes)
|
|
349
|
+
* then delegates to the materialized value. The array target keeps `Array.isArray`
|
|
350
|
+
* true for a list root, matching a non-deferred list read; the materialized value
|
|
351
|
+
* is memoized so repeated sync reads on the same returned value keep stable element
|
|
352
|
+
* identity. When no async executor is wired `.then` is absent, so the value is a
|
|
353
|
+
* plain (non-thenable) sync read — unchanged React behavior.
|
|
354
|
+
*/
|
|
355
|
+
function makeDeferredRootValue(
|
|
356
|
+
rootField: string,
|
|
357
|
+
args: Record<string, unknown> | undefined,
|
|
358
|
+
isList: boolean,
|
|
359
|
+
materialize: (seeded: FieldValue) => unknown,
|
|
360
|
+
syncResolve: (rootField: string, args: Record<string, unknown> | undefined) => FieldValue,
|
|
361
|
+
asyncResolve?: (rootField: string, args: Record<string, unknown> | undefined) => Promise<FieldValue>,
|
|
362
|
+
): unknown {
|
|
363
|
+
const then = asyncResolve
|
|
364
|
+
? (onFulfilled?: (v: unknown) => unknown, onRejected?: (e: unknown) => unknown) =>
|
|
365
|
+
asyncResolve(rootField, args).then((seeded) => materialize(seeded)).then(onFulfilled, onRejected)
|
|
366
|
+
: undefined;
|
|
367
|
+
|
|
368
|
+
// Materialize once per resolved value: `syncResolve` throws the Suspense promise
|
|
369
|
+
// until seeded, so nothing is cached until it succeeds; afterwards repeated reads
|
|
370
|
+
// return the same array/proxies (stable identity, matching a plain array/object).
|
|
371
|
+
let ready = false;
|
|
372
|
+
let value: unknown;
|
|
373
|
+
const syncValue = (): Record<PropertyKey, unknown> => {
|
|
374
|
+
if (!ready) {
|
|
375
|
+
value = materialize(syncResolve(rootField, args));
|
|
376
|
+
ready = true;
|
|
377
|
+
}
|
|
378
|
+
return value as Record<PropertyKey, unknown>;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// A real array target for list roots (so `Array.isArray` is true and index/length
|
|
382
|
+
// reads behave like the array they materialize to); a plain object otherwise. Both
|
|
383
|
+
// are empty and carry no own keys, so enumeration/spread never leak internals.
|
|
384
|
+
const target: object = isList ? [] : {};
|
|
385
|
+
|
|
386
|
+
return new Proxy(target, {
|
|
387
|
+
get(_t, prop) {
|
|
388
|
+
if (prop === "then") return then;
|
|
389
|
+
// Any other read sync-resolves (Suspense) then delegates to the materialized
|
|
390
|
+
// value/array. Bind functions (Array.prototype.* / callable graph fields) to
|
|
391
|
+
// that value so `this` is correct.
|
|
392
|
+
const v = syncValue();
|
|
393
|
+
const got = v[prop];
|
|
394
|
+
return typeof got === "function" ? (got as (...a: unknown[]) => unknown).bind(v) : got;
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -107,6 +107,46 @@ export class GraphRuntime {
|
|
|
107
107
|
throw entry.promise;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Async twin of {@link resolveRoot} for non-React callers (server handlers,
|
|
112
|
+
* proxy): run `exec` once per `key` and RESOLVE to the seeded root refs instead
|
|
113
|
+
* of throwing a Suspense promise. Shares the `resolvedRoots` cache and the
|
|
114
|
+
* `pending` map with `resolveRoot`, so an `await` and a concurrent sync (thrown)
|
|
115
|
+
* read of the same root+args dedupe to a single in-flight fetch.
|
|
116
|
+
*/
|
|
117
|
+
async resolveRootAsync(key: string, exec: () => Promise<Record<string, FieldValue>>): Promise<Record<string, FieldValue>> {
|
|
118
|
+
const done = this.resolvedRoots.get(key);
|
|
119
|
+
if (done) return done;
|
|
120
|
+
|
|
121
|
+
const pkey = `@root:${key}`;
|
|
122
|
+
const existing = this.pending.get(pkey);
|
|
123
|
+
if (existing) {
|
|
124
|
+
// A sync read already kicked off this fetch — await it, then read the cache.
|
|
125
|
+
await existing.promise;
|
|
126
|
+
return this.resolvedRoots.get(key) ?? {};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const entry = this.makeDeferred();
|
|
130
|
+
// The barrier promise only exists to signal concurrent readers (a sync reader
|
|
131
|
+
// `throw`s it, an async reader `await`s it). On the solo path nothing attaches
|
|
132
|
+
// to it, so observe its rejection here — the error still propagates to THIS
|
|
133
|
+
// caller via the `throw` below; without this a failing `await glean.x()` would
|
|
134
|
+
// surface an unhandled rejection (noisy / isolate-fatal in a Worker).
|
|
135
|
+
entry.promise.catch(() => {});
|
|
136
|
+
this.pending.set(pkey, entry);
|
|
137
|
+
try {
|
|
138
|
+
const roots = await exec();
|
|
139
|
+
this.resolvedRoots.set(key, roots);
|
|
140
|
+
this.pending.delete(pkey);
|
|
141
|
+
entry.resolve();
|
|
142
|
+
return roots;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
this.pending.delete(pkey);
|
|
145
|
+
entry.reject(error);
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
110
150
|
/** Seed a record's fields (e.g. from the compiled operation result). */
|
|
111
151
|
seed(ref: GraphRef, fields: Readonly<Record<string, FieldValue>>): void {
|
|
112
152
|
this.cache.merge(ref, fields);
|