foldkit-remote 0.1.0 → 0.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.
package/dist/index.mjs CHANGED
@@ -1,7 +1,110 @@
1
1
  import { Context, Deferred, Duration, Effect, Layer, Option, Request, RequestResolver, Result, Schema, SchemaGetter, Stream } from "effect";
2
- import { Requirement } from "foldkit-surface";
2
+ import { Metadata } from "foldkit-surface";
3
3
  import { Rpc, RpcGroup } from "effect/unstable/rpc";
4
4
  import { KeyValueStore } from "effect/unstable/persistence";
5
+ //#region src/requirement.ts
6
+ /**
7
+ * What a Remote Projection needs from the server: entity slices and query
8
+ * connections. They ride on `Projection.metadata` under Remote's own keys, so
9
+ * Surface composes them without knowing their shape.
10
+ */
11
+ const windowKey$1 = (window) => JSON.stringify([
12
+ window.first ?? null,
13
+ window.last ?? null,
14
+ window.after ?? null,
15
+ window.before ?? null
16
+ ]);
17
+ /**
18
+ * Merges connection requirements for the same connection and window into one,
19
+ * unioning what they select of each item; the first keeps its other properties.
20
+ */
21
+ function mergeConnections(connections) {
22
+ const merged = /* @__PURE__ */ new Map();
23
+ for (const connection of connections) {
24
+ const key = `${connection.identity}\u0000${windowKey$1(connection.window)}`;
25
+ const current = merged.get(key);
26
+ merged.set(key, current === void 0 ? connection : {
27
+ ...current,
28
+ select: mergeRelation(current.select, connection.select)
29
+ });
30
+ }
31
+ return [...merged.values()];
32
+ }
33
+ /** Unions two relation slices for the same target: fields, windows, and nested relations. */
34
+ function mergeRelation(current, next) {
35
+ const fields = [...current.fields];
36
+ const seen = new Set(fields);
37
+ for (const field of next.fields) {
38
+ if (seen.has(field)) continue;
39
+ seen.add(field);
40
+ fields.push(field);
41
+ }
42
+ const windows = {
43
+ ...current.windows,
44
+ ...next.windows
45
+ };
46
+ const relations = mergeRelations(current.relations, next.relations);
47
+ return {
48
+ entity: current.entity,
49
+ fields,
50
+ ...Object.keys(windows).length === 0 ? {} : { windows },
51
+ ...relations === void 0 ? {} : { relations }
52
+ };
53
+ }
54
+ /** Unions two relation maps field by field; a field in both merges recursively. */
55
+ function mergeRelations(current, next) {
56
+ if (current === void 0) return next;
57
+ if (next === void 0) return current;
58
+ const merged = { ...current };
59
+ for (const [field, relation] of Object.entries(next)) {
60
+ const existing = merged[field];
61
+ merged[field] = existing === void 0 ? relation : mergeRelation(existing, relation);
62
+ }
63
+ return merged;
64
+ }
65
+ /** Unions requirements for the same entity + id, dropping duplicate fields. */
66
+ function mergeRequirements(requirements) {
67
+ const grouped = /* @__PURE__ */ new Map();
68
+ for (const requirement of requirements) {
69
+ const key = `${requirement.entity}\u0000${requirement.id}`;
70
+ const group = grouped.get(key);
71
+ const live = group?.live === true || requirement.live === true;
72
+ grouped.set(key, {
73
+ ...mergeRelation(group ?? {
74
+ entity: requirement.entity,
75
+ fields: []
76
+ }, requirement),
77
+ id: requirement.id,
78
+ ...live ? { live } : {}
79
+ });
80
+ }
81
+ return [...grouped.values()];
82
+ }
83
+ /**
84
+ * Requirement helpers, for the packages that plan and serve requirements.
85
+ * `merge` unions same-entity+id requirements; `mergeRelation` unions two
86
+ * slices of one target (fields, windows, nested relations).
87
+ */
88
+ const Requirement = {
89
+ merge: mergeRequirements,
90
+ mergeRelation,
91
+ mergeConnections
92
+ };
93
+ /** Remote's entity requirements on a Projection, unioned per entity and id as nodes compose. */
94
+ const RemoteRequirements = Metadata.key("remote", {
95
+ merge: mergeRequirements,
96
+ summarize: (requirement) => `${requirement.entity}:${requirement.id}`
97
+ });
98
+ /** Remote's query connections on a Projection, unioned per connection and window. */
99
+ const RemoteConnections = Metadata.key("remote.connection", {
100
+ merge: mergeConnections,
101
+ summarize: (connection) => connection.identity.split("\0").join(" ")
102
+ });
103
+ /** The entity requirements a Projection carries. */
104
+ const requirementsOf = (projection) => RemoteRequirements.get(projection.metadata);
105
+ /** The query connections a Projection carries. */
106
+ const connectionsOf = (projection) => RemoteConnections.get(projection.metadata);
107
+ //#endregion
5
108
  //#region src/store.ts
6
109
  /**
7
110
  * The pure entity store: values, per-field presence, staleness, and tombstones.
@@ -80,6 +183,7 @@ const setStale = (store, marks, stale) => {
80
183
  for (const field of fields) if (stale) {
81
184
  if (previous.present.has(field)) staleFields.add(field);
82
185
  } else staleFields.delete(field);
186
+ if (staleFields.size === previous.stale.size) continue;
83
187
  next ??= { ...store };
84
188
  next[key] = {
85
189
  ...previous,
@@ -1463,14 +1567,10 @@ const parseEntry = (value) => {
1463
1567
  };
1464
1568
  };
1465
1569
  const deserializeStore = (serialized) => Object.fromEntries(Object.entries(serialized.entities).map(([key, entry]) => [key, parseEntry(entry)]));
1466
- /**
1467
- * The snapshot text, or `undefined` when it would exceed `maxBytes`. Only the
1468
- * entity store goes in; pass `model.entities`, never the whole `RemoteModel`.
1469
- */
1470
- const dehydrate = (store, options = {}) => {
1570
+ function dehydrate(store, options = {}) {
1471
1571
  const text = stableStringify(serializeStore(store, options.scope));
1472
1572
  return exceeds(text, options.maxBytes) ? void 0 : text;
1473
- };
1573
+ }
1474
1574
  /**
1475
1575
  * Whether the UTF-8 size of `text` exceeds `maxBytes`. A character is one to
1476
1576
  * three bytes, so most texts are decided from their length alone; only the
@@ -1641,7 +1741,12 @@ const initialRemoteModel = {
1641
1741
  optimistic: emptyOptimistic,
1642
1742
  live: {},
1643
1743
  mutations: emptyMutationState,
1644
- gaps: /* @__PURE__ */ new Set()
1744
+ gaps: /* @__PURE__ */ new Set(),
1745
+ loading: /* @__PURE__ */ new Set(),
1746
+ refresh: {
1747
+ requested: 0,
1748
+ started: 0
1749
+ }
1645
1750
  };
1646
1751
  /** The entity store and the mutation ledger are runtime values, not wire shapes. */
1647
1752
  const runtimeSchema = Schema.Unknown;
@@ -1651,7 +1756,12 @@ const remoteModelSchema = () => Schema.Struct({
1651
1756
  optimistic: runtimeSchema,
1652
1757
  live: Schema.Record(Schema.String, runtimeSchema),
1653
1758
  mutations: runtimeSchema,
1654
- gaps: runtimeSchema
1759
+ gaps: runtimeSchema,
1760
+ loading: runtimeSchema,
1761
+ refresh: Schema.Struct({
1762
+ requested: Schema.Number,
1763
+ started: Schema.Number
1764
+ })
1655
1765
  });
1656
1766
  const retentionRootsSchema = Schema.Struct({
1657
1767
  requirements: Schema.Array(ReadRequest),
@@ -1675,6 +1785,10 @@ const remoteMessageCases = {
1675
1785
  error: remoteErrorSchema
1676
1786
  },
1677
1787
  RefreshStarted: { requests: Schema.Array(ReadRequest) },
1788
+ ReadStarted: {
1789
+ requests: Schema.Array(ReadRequest),
1790
+ refresh: Schema.optional(Schema.Number)
1791
+ },
1678
1792
  RetentionChanged: { roots: retentionRootsSchema },
1679
1793
  Hydrated: {
1680
1794
  entities: runtimeSchema,
@@ -1722,6 +1836,39 @@ const remoteMessageSchema = Schema.Union(Object.entries(remoteMessageCases).map(
1722
1836
  ...fields
1723
1837
  })));
1724
1838
  const marksOf = (requests) => requests.map((request) => [entityKey(request.entity, request.id), request.fields]);
1839
+ /**
1840
+ * One mark per requested field. A read is identified by what it asks for, not by
1841
+ * a request id: two reads asking for one field share its mark, so the first
1842
+ * answer clears it. Over-clearing shows `Initial` rather than a spinner, which
1843
+ * is the safer way to be wrong.
1844
+ */
1845
+ const loadingMarks = function* (requests) {
1846
+ for (const request of requests) {
1847
+ const key = entityKey(request.entity, request.id);
1848
+ for (const field of request.fields) yield `${key}\u0000${field}`;
1849
+ }
1850
+ };
1851
+ const withLoading = (loading, requests) => {
1852
+ const next = new Set(loading);
1853
+ for (const mark of loadingMarks(requests)) next.add(mark);
1854
+ return next;
1855
+ };
1856
+ const withoutLoading = (loading, requests) => {
1857
+ if (loading.size === 0) return loading;
1858
+ const next = new Set(loading);
1859
+ for (const mark of loadingMarks(requests)) next.delete(mark);
1860
+ return next;
1861
+ };
1862
+ /**
1863
+ * Whether a read is fetching any of `fields` for this entity. Consulted only
1864
+ * when the store lacks the value, which is what separates `Loading` from
1865
+ * `Initial`.
1866
+ */
1867
+ const isLoading = (model, entity, id, fields) => {
1868
+ if (model.loading.size === 0) return false;
1869
+ const key = entityKey(entity, id);
1870
+ return fields.some((field) => model.loading.has(`${key}\u0000${field}`));
1871
+ };
1725
1872
  const setConnectionStale = (connections, connection, stale) => {
1726
1873
  const current = connections[connection] ?? emptyConnection;
1727
1874
  return {
@@ -1749,11 +1896,13 @@ const updateRemote = (model, message) => {
1749
1896
  switch (message._tag) {
1750
1897
  case "ReadReceived": return {
1751
1898
  ...model,
1752
- entities: writeRead(model.entities, message.requests, message.result, message.now)
1899
+ entities: writeRead(model.entities, message.requests, message.result, message.now),
1900
+ loading: withoutLoading(model.loading, message.requests)
1753
1901
  };
1754
1902
  case "ReadFailed": return {
1755
1903
  ...model,
1756
- entities: setStale(model.entities, marksOf(message.requests), false)
1904
+ entities: setStale(model.entities, marksOf(message.requests), false),
1905
+ loading: withoutLoading(model.loading, message.requests)
1757
1906
  };
1758
1907
  case "RetentionChanged": return {
1759
1908
  ...model,
@@ -1763,10 +1912,24 @@ const updateRemote = (model, message) => {
1763
1912
  ...model,
1764
1913
  entities: RemotePersistence.mergeStores(model.entities, message.entities, message.merge)
1765
1914
  };
1766
- case "RefreshStarted": return {
1767
- ...model,
1768
- entities: setStale(model.entities, marksOf(message.requests), true)
1769
- };
1915
+ case "RefreshStarted": {
1916
+ const entities = setStale(model.entities, marksOf(message.requests), true);
1917
+ return entities === model.entities ? model : {
1918
+ ...model,
1919
+ entities
1920
+ };
1921
+ }
1922
+ case "ReadStarted": {
1923
+ const started = Math.max(model.refresh.started, message.refresh ?? 0);
1924
+ return {
1925
+ ...model,
1926
+ loading: withLoading(model.loading, message.requests),
1927
+ refresh: started === model.refresh.started ? model.refresh : {
1928
+ ...model.refresh,
1929
+ started
1930
+ }
1931
+ };
1932
+ }
1770
1933
  case "MutationStarted": return {
1771
1934
  ...model,
1772
1935
  mutations: beginMutation(model.mutations, message.requestId),
@@ -1812,7 +1975,8 @@ const updateRemote = (model, message) => {
1812
1975
  case "GapCleared": return clearGap(model, message.stream);
1813
1976
  case "ConnectionMerged": {
1814
1977
  const current = model.connections[message.connection] ?? emptyConnection;
1815
- const merged = merge(current, message.page);
1978
+ const replaces = message.refreshes === true && current.stale;
1979
+ const merged = merge(replaces ? emptyConnection : current, message.page);
1816
1980
  return {
1817
1981
  ...model,
1818
1982
  connections: {
@@ -1825,10 +1989,12 @@ const updateRemote = (model, message) => {
1825
1989
  optimistic: pruneOverlays(model.optimistic, message.connection, new Set(message.page.edges.map((edge) => edge.key)), model.mutations.pending)
1826
1990
  };
1827
1991
  }
1828
- case "ConnectionInvalidated": return {
1829
- ...model,
1830
- connections: setConnectionStale(model.connections, message.connection, true)
1831
- };
1992
+ case "ConnectionInvalidated":
1993
+ if (model.connections[message.connection]?.stale === true) return model;
1994
+ return {
1995
+ ...model,
1996
+ connections: setConnectionStale(model.connections, message.connection, true)
1997
+ };
1832
1998
  case "QueryFailed": return message.connection in model.connections ? {
1833
1999
  ...model,
1834
2000
  connections: setConnectionStale(model.connections, message.connection, false)
@@ -2246,7 +2412,7 @@ const bindRemote = (definition, store) => ({
2246
2412
  owns: store.dependency.length === 0 ? [] : [store.dependency],
2247
2413
  observes: store.dependency.length === 0 ? [] : [store.dependency],
2248
2414
  messages: [],
2249
- requirements: []
2415
+ metadata: []
2250
2416
  }
2251
2417
  });
2252
2418
  /** The runtime half of `Registered`: a descriptor the domain never declared is an error naming both. */
@@ -2257,7 +2423,10 @@ const nothingAsked = {
2257
2423
  requirements: [],
2258
2424
  connections: []
2259
2425
  };
2260
- const askedOf = (projection) => projection === void 0 ? nothingAsked : projection;
2426
+ const askedOf = (projection) => projection === void 0 ? nothingAsked : {
2427
+ requirements: requirementsOf(projection),
2428
+ connections: connectionsOf(projection)
2429
+ };
2261
2430
  /**
2262
2431
  * Plans what a projection asks against the Model. A connection the Model holds
2263
2432
  * fresh contributes its visible items' selected fields to the entity plan; one
@@ -2333,7 +2502,7 @@ const pageMessage = (connection, result, refreshes = false) => ({
2333
2502
  const rootsOf = (projections, options) => {
2334
2503
  const connections = /* @__PURE__ */ new Map();
2335
2504
  for (const identity of (options.connections ?? []).map(connectionIdentity)) connections.set(identity, { identity });
2336
- for (const { identity, select } of projections.flatMap((projection) => projection.connections)) {
2505
+ for (const { identity, select } of projections.flatMap(connectionsOf)) {
2337
2506
  const current = connections.get(identity)?.select;
2338
2507
  connections.set(identity, {
2339
2508
  identity,
@@ -2341,10 +2510,39 @@ const rootsOf = (projections, options) => {
2341
2510
  });
2342
2511
  }
2343
2512
  return {
2344
- requirements: Requirement.merge(projections.flatMap((projection) => projection.requirements)),
2513
+ requirements: Requirement.merge(projections.flatMap(requirementsOf)),
2345
2514
  connections: [...connections.values()].sort((a, b) => a.identity < b.identity ? -1 : 1)
2346
2515
  };
2347
2516
  };
2517
+ /** Reads requirements and reports the Message that settles them. Never fails. */
2518
+ const readMessage = (requirements, now) => Effect.gen(function* () {
2519
+ const client = yield* RemoteClient;
2520
+ const at = now();
2521
+ const result = yield* Effect.result(client.read({
2522
+ version: 3,
2523
+ requests: requirements
2524
+ }));
2525
+ return Result.isFailure(result) ? {
2526
+ _tag: "ReadFailed",
2527
+ requests: requirements,
2528
+ error: remoteError(result.failure)
2529
+ } : {
2530
+ _tag: "ReadReceived",
2531
+ requests: requirements,
2532
+ result: result.success,
2533
+ now: at
2534
+ };
2535
+ });
2536
+ /** Runs a query and reports the page that refreshes its connection, or the failure. Never fails. */
2537
+ const queryMessage = (query) => Effect.gen(function* () {
2538
+ const client = yield* RemoteClient;
2539
+ const result = yield* Effect.result(queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request))));
2540
+ return Result.isFailure(result) ? {
2541
+ _tag: "QueryFailed",
2542
+ connection: query.identity,
2543
+ error: remoteError(result.failure)
2544
+ } : pageMessage(query.identity, result.success, true);
2545
+ });
2348
2546
  /** A query the read entry runs, as its dependencies carry it: plain data Foldkit compares. */
2349
2547
  const PlannedQuery = Schema.Struct({
2350
2548
  identity: Schema.String,
@@ -2362,41 +2560,19 @@ const PlannedQuery = Schema.Struct({
2362
2560
  */
2363
2561
  const observeEntry = (bound, askedOf, toMessage, options) => {
2364
2562
  const { policy = RemotePolicy.cacheFirst, now = Date.now } = options;
2365
- const read = (requirements) => Effect.gen(function* () {
2366
- const client = yield* RemoteClient;
2367
- const at = now();
2368
- const result = yield* Effect.result(client.read({
2369
- version: 3,
2370
- requests: requirements
2371
- }));
2372
- return Result.isFailure(result) ? toMessage({
2373
- _tag: "ReadFailed",
2374
- requests: requirements,
2375
- error: remoteError(result.failure)
2376
- }) : toMessage({
2377
- _tag: "ReadReceived",
2378
- requests: requirements,
2379
- result: result.success,
2380
- now: at
2381
- });
2382
- });
2383
- const run = (query) => Effect.gen(function* () {
2384
- const client = yield* RemoteClient;
2385
- const result = yield* Effect.result(queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request))));
2386
- return Result.isFailure(result) ? toMessage({
2387
- _tag: "QueryFailed",
2388
- connection: query.identity,
2389
- error: remoteError(result.failure)
2390
- }) : toMessage(pageMessage(query.identity, result.success, true));
2391
- });
2563
+ const read = (requirements) => Effect.map(readMessage(requirements, now), toMessage);
2564
+ const run = (query) => Effect.map(queryMessage(query), toMessage);
2392
2565
  return {
2393
2566
  dependenciesSchema: Schema.Struct({
2394
2567
  requirements: Schema.Array(ReadRequest),
2395
- queries: Schema.Array(PlannedQuery)
2568
+ queries: Schema.Array(PlannedQuery),
2569
+ refresh: Schema.Number
2396
2570
  }),
2397
2571
  modelToDependencies: (model) => {
2398
- const planned = planAsked(bound.store.get(model), askedOf(model), RemotePolicy.toPlan(policy, now()));
2572
+ const remote = bound.store.get(model);
2573
+ const planned = planAsked(remote, askedOf(model), RemotePolicy.toPlan(policy, now()));
2399
2574
  return {
2575
+ refresh: remote.refresh.requested,
2400
2576
  requirements: planned.requirements,
2401
2577
  queries: planned.queries.map(({ identity, window, select }) => ({
2402
2578
  identity,
@@ -2405,10 +2581,14 @@ const observeEntry = (bound, askedOf, toMessage, options) => {
2405
2581
  }))
2406
2582
  };
2407
2583
  },
2408
- dependenciesToStream: ({ requirements, queries }) => Stream.concat(RemotePolicy.refreshes(policy) && requirements.length > 0 ? Stream.succeed(toMessage({
2584
+ dependenciesToStream: ({ requirements, queries, refresh }) => Stream.concat(requirements.length === 0 ? Stream.empty : Stream.fromIterable([toMessage({
2585
+ _tag: "ReadStarted",
2586
+ requests: requirements,
2587
+ refresh
2588
+ }), ...RemotePolicy.refreshes(policy) ? [toMessage({
2409
2589
  _tag: "RefreshStarted",
2410
2590
  requests: requirements
2411
- })) : Stream.empty, Stream.mergeAll([...requirements.length === 0 ? [] : [Stream.fromEffect(read(requirements))], ...queries.map((query) => Stream.fromEffect(run(query)))], { concurrency: "unbounded" }))
2591
+ })] : []]), Stream.mergeAll([...requirements.length === 0 ? [] : [Stream.fromEffect(read(requirements))], ...queries.map((query) => Stream.fromEffect(run(query)))], { concurrency: "unbounded" }))
2412
2592
  };
2413
2593
  };
2414
2594
  /** The live entry: subscribes to `requirementsOf(model)` from the Model's resume cursor. */
@@ -2504,18 +2684,17 @@ const Remote = {
2504
2684
  return (id) => ({
2505
2685
  Model: remoteDataSchema(selection.schema),
2506
2686
  dependencies: [],
2507
- requirements: [{
2687
+ metadata: RemoteRequirements.of({
2508
2688
  ...relation,
2509
2689
  id
2510
- }],
2511
- connections: [],
2690
+ }),
2512
2691
  read: (root) => {
2513
2692
  const store = storeOf(bound, root);
2514
2693
  const key = entityKey(selection.entity, id);
2515
- return memoRead(store, store, `${key}\u0000${stableStringify(relation)}`, () => {
2694
+ const present = memoRead(store, store, `${key}\u0000${stableStringify(relation)}`, () => {
2516
2695
  if (isTombstone(store, key)) return { _tag: "NotFound" };
2517
2696
  const assembled = assemble(store, key, relation);
2518
- if (assembled === void 0) return { _tag: "Initial" };
2697
+ if (assembled === void 0) return void 0;
2519
2698
  const decoded = Schema.decodeUnknownResult(selection.schema)(assembled.values);
2520
2699
  return Result.isFailure(decoded) ? {
2521
2700
  _tag: "Failed",
@@ -2531,6 +2710,8 @@ const Remote = {
2531
2710
  value: decoded.success
2532
2711
  };
2533
2712
  });
2713
+ if (present !== void 0) return present;
2714
+ return isLoading(bound.store.get(root), selection.entity, id, relation.fields) ? { _tag: "Loading" } : { _tag: "Initial" };
2534
2715
  }
2535
2716
  });
2536
2717
  },
@@ -2539,12 +2720,12 @@ const Remote = {
2539
2720
  * remote store does not satisfy, under `options` (freshness, force). A
2540
2721
  * Surface's projection is `surface.projection(params)`.
2541
2722
  */
2542
- plan: (bound, model, projection, options) => planAsked(bound.store.get(model), projection, options ?? {}).requirements,
2723
+ plan: (bound, model, projection, options) => planAsked(bound.store.get(model), askedOf(projection), options ?? {}).requirements,
2543
2724
  /**
2544
2725
  * The queries a projection needs run before its connections read: those the
2545
2726
  * Model does not hold, or holds stale (every one under `force`).
2546
2727
  */
2547
- planQueries: (bound, model, projection, options) => planAsked(bound.store.get(model), projection, options ?? {}).queries.flatMap((query) => query.ref === void 0 ? [] : [query.ref]),
2728
+ planQueries: (bound, model, projection, options) => planAsked(bound.store.get(model), askedOf(projection), options ?? {}).queries.flatMap((query) => query.ref === void 0 ? [] : [query.ref]),
2548
2729
  /**
2549
2730
  * The store reads see: the base store under the pending optimistic layers.
2550
2731
  * One store is shared by every read and plan of a Model whose remote state
@@ -2561,7 +2742,7 @@ const Remote = {
2561
2742
  const store = storeOf(bound, model);
2562
2743
  const { policy = RemotePolicy.cacheFirst, now = Date.now } = options;
2563
2744
  const at = now();
2564
- const missing = plan(store, projection.requirements, RemotePolicy.toPlan(policy, at));
2745
+ const missing = plan(store, requirementsOf(projection), RemotePolicy.toPlan(policy, at));
2565
2746
  if (missing.length === 0) return store;
2566
2747
  yield* Effect.annotateCurrentSpan("requirementCount", missing.length);
2567
2748
  const result = yield* (yield* RemoteClient).read({
@@ -2571,6 +2752,52 @@ const Remote = {
2571
2752
  return writeRead(store, missing, result, at);
2572
2753
  }),
2573
2754
  /**
2755
+ * Marks what a projection, or a Surface without params, requires as due again.
2756
+ * Called from `update`: every selected field the store holds reads `Refreshing`,
2757
+ * and every loaded query connection is invalidated. Nothing is fetched here.
2758
+ * The read entries `Data.subscriptions` derives refetch it, because a stale
2759
+ * field or connection is planned again under every policy, so a refresh never
2760
+ * sends a request beside the entry already observing the same data.
2761
+ *
2762
+ * A refreshed connection's page replaces its pages, so items the server removed
2763
+ * or reordered follow it; pages loaded past the first are dropped and paged
2764
+ * again with `next`/`previous`. The read entries restart, so a read or query
2765
+ * sent before the refresh is not applied after it.
2766
+ *
2767
+ * The projection must be observed, which it is while it is on screen. Live
2768
+ * subscriptions are left as they are. For data nothing observes (SSR, tests),
2769
+ * use `Remote.prefetch` with `RemotePolicy.networkOnly`. Refreshing what is
2770
+ * already being refreshed returns the same Model.
2771
+ */
2772
+ refresh: (bound, model, target) => {
2773
+ const projection = "read" in target ? target : target.projection(void 0);
2774
+ const remote = bound.store.get(model);
2775
+ const asked = askedOf(projection);
2776
+ const connections = Requirement.mergeConnections(asked.connections);
2777
+ const items = connections.flatMap((connection) => {
2778
+ const known = remote.connections[connection.identity];
2779
+ return known === void 0 ? [] : itemsOf(visibleItems(known, connection.identity, remote.optimistic.overlays, remote.entities).map((edge) => edge.ref), connection.select);
2780
+ });
2781
+ const requirements = plan(visibleStoreOf(remote.entities, remote.optimistic), [...asked.requirements, ...items], { force: true });
2782
+ const marked = [...requirements.length === 0 ? [] : [{
2783
+ _tag: "RefreshStarted",
2784
+ requests: requirements
2785
+ }], ...connections.filter((connection) => remote.connections[connection.identity] !== void 0).map((connection) => ({
2786
+ _tag: "ConnectionInvalidated",
2787
+ connection: connection.identity
2788
+ }))].reduce(updateRemote, remote);
2789
+ const inFlight = remote.refresh.started === remote.refresh.requested && requirements.some((requirement) => requirement.fields.some((field) => remote.entities[entityKey(requirement.entity, requirement.id)]?.stale.has(field) === true));
2790
+ if (marked === remote && !inFlight) return model;
2791
+ const refresh = {
2792
+ ...marked.refresh,
2793
+ requested: marked.refresh.requested + 1
2794
+ };
2795
+ return bound.store.set(model, {
2796
+ ...marked,
2797
+ refresh
2798
+ });
2799
+ },
2800
+ /**
2574
2801
  * Writes a read result into the store, recording each field's applied window.
2575
2802
  * `requests` are the planned requirements the result answers.
2576
2803
  */
@@ -2648,7 +2875,7 @@ const Remote = {
2648
2875
  /**
2649
2876
  * Runs a mutation through `RemoteClient`, decoding its typed Output and
2650
2877
  * returning the result's normalized patches so the caller can reconcile them
2651
- * through `Remote.update`'s `MutationSucceeded` (or `Remote.mutateInto`).
2878
+ * through `updateRemote`'s `MutationSucceeded` (or `Remote.mutateInto`).
2652
2879
  */
2653
2880
  mutate: mutateRemote,
2654
2881
  /**
@@ -2684,14 +2911,14 @@ const Remote = {
2684
2911
  * emits `RefreshStarted`, so the fields being refetched read as `Refreshing`
2685
2912
  * while the request is pending.
2686
2913
  */
2687
- observe: (bound, surface, params, toMessage = identityMessage, options = {}) => observeEntry(bound, () => surface.projection(params), toMessage, options),
2914
+ observe: (bound, surface, params, toMessage = identityMessage, options = {}) => observeEntry(bound, () => askedOf(surface.projection(params)), toMessage, options),
2688
2915
  /**
2689
2916
  * A Foldkit Subscription entry that consumes the live stream for a Surface's
2690
2917
  * requirements, emitting a `LiveReceived` per event and a `ReadFailed` when
2691
2918
  * the stream breaks (including `ResumeUnavailable`). The resume cursor is read
2692
2919
  * from `RemoteModel.live`, so the application tracks no cursor of its own.
2693
2920
  */
2694
- live: (bound, surface, params, toMessage = identityMessage, options = {}) => liveEntry(bound, () => surface.projection(params).requirements, toMessage, options)
2921
+ live: (bound, surface, params, toMessage = identityMessage, options = {}) => liveEntry(bound, () => requirementsOf(surface.projection(params)), toMessage, options)
2695
2922
  };
2696
2923
  /** The bound domain: the descriptor, the binding, and the operations over them. */
2697
2924
  const bindDomain = (definition, store) => {
@@ -2705,10 +2932,10 @@ const bindDomain = (definition, store) => {
2705
2932
  const projection = Remote.select(bound, selection)(id);
2706
2933
  return {
2707
2934
  ...projection,
2708
- requirements: projection.requirements.map((requirement) => ({
2935
+ metadata: RemoteRequirements.of(...requirementsOf(projection).map((requirement) => ({
2709
2936
  ...requirement,
2710
2937
  live: true
2711
- }))
2938
+ })))
2712
2939
  };
2713
2940
  },
2714
2941
  subscriptions: (active, options = {}) => {
@@ -2742,11 +2969,11 @@ const bindDomain = (definition, store) => {
2742
2969
  const client = yield* RemoteClient;
2743
2970
  const planOptions = RemotePolicy.toPlan(policy, now());
2744
2971
  let current = model;
2745
- for (const query of planAsked(store.get(current), projection, planOptions).queries) {
2972
+ for (const query of planAsked(store.get(current), askedOf(projection), planOptions).queries) {
2746
2973
  const page = yield* queryRequestOf(query).pipe(Effect.flatMap((request) => client.query(request)));
2747
2974
  current = reduce(current, pageMessage(query.identity, page, true));
2748
2975
  }
2749
- const requirements = planAsked(store.get(current), projection, planOptions).requirements;
2976
+ const requirements = planAsked(store.get(current), askedOf(projection), planOptions).requirements;
2750
2977
  if (requirements.length === 0) return current;
2751
2978
  const at = now();
2752
2979
  const result = yield* client.read({
@@ -2782,8 +3009,7 @@ const bindDomain = (definition, store) => {
2782
3009
  return {
2783
3010
  Model: remoteDataSchema(pageSchema(select.schema)),
2784
3011
  dependencies: [],
2785
- requirements: [],
2786
- connections: [requirement],
3012
+ metadata: RemoteConnections.of(requirement),
2787
3013
  ref,
2788
3014
  read: (root) => {
2789
3015
  const remote = store.get(root);
@@ -2845,6 +3071,7 @@ const bindDomain = (definition, store) => {
2845
3071
  }
2846
3072
  } : void 0;
2847
3073
  },
3074
+ refresh: (model, target) => Remote.refresh(bound, model, target),
2848
3075
  fetch: (ref) => ({
2849
3076
  name: `Remote.query(${ref.query})`,
2850
3077
  args: {
@@ -2914,6 +3141,6 @@ const pageSize = (window, key) => {
2914
3141
  return size === void 0 ? {} : { [key]: size };
2915
3142
  };
2916
3143
  //#endregion
2917
- export { ConnectionChange, ConnectionChangeSchema, Entity, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_RELATION_DEPTH, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, PageSize, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, REMOTE_CACHE_VERSION, REMOTE_PROTOCOL_VERSION, Read, ReadBatch, ReadBatchResult, ReadRequest, RelationAnnotation, RelationEntityAnnotation, RelationRequest, Remote, RemoteClient, RemoteData, RemoteLiveError, RemoteMutationError, RemotePersistence, RemotePolicy, RemoteProtocolError, RemoteQueryError, RemoteReadError, RemoteRpc, Selection, WindowSchema, WireBoundary, addLayer, addOverlay, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, cursor, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyStore, entityKey, entry, failMutation, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote, invalidateConnection, isFieldStale, isGapped, isRefPage, isRemoteMessage, isStale, isTombstone, items, liveEventOf, liveHasNext, liveHasPrevious, markStale, merge, missingFields, mutateRemote, pageSchema, plan, pruneOverlays, reachable, readField, reconcileMutation, refParts, refreshConnection, refsIn, relationOf, relationShape, remoteDataSchema, remoteError, remoteErrorSchema, remoteMessageCases, remoteMessageSchema, remoteModelSchema, remove, removeLayer, removeOverlay, requirementKey, retentionRootsSchema, schemaOf, segment, setStale, settleFailure, settleSuccess, shouldWake, stableStringify, targetsOf, terminal, tombstone, unknown, updateRemote, visibleItems, visibleStore, windowKey, writeEntities, writeEntity, writeRead };
3144
+ export { ConnectionChange, ConnectionChangeSchema, Entity, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_RELATION_DEPTH, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, PageSize, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, REMOTE_CACHE_VERSION, REMOTE_PROTOCOL_VERSION, Read, ReadBatch, ReadBatchResult, ReadRequest, RelationAnnotation, RelationEntityAnnotation, RelationRequest, Remote, RemoteClient, RemoteConnections, RemoteData, RemoteLiveError, RemoteMutationError, RemotePersistence, RemotePolicy, RemoteProtocolError, RemoteQueryError, RemoteReadError, RemoteRequirements, RemoteRpc, Requirement, Selection, WindowSchema, WireBoundary, addLayer, addOverlay, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyStore, entityKey, entry, failMutation, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote, invalidateConnection, isFieldStale, isGapped, isLoading, isRefPage, isRemoteMessage, isStale, isTombstone, items, liveEventOf, liveHasNext, liveHasPrevious, markStale, merge, missingFields, mutateRemote, pageSchema, plan, pruneOverlays, reachable, readField, reconcileMutation, refParts, refreshConnection, refsIn, relationOf, relationShape, remoteDataSchema, remoteError, remoteErrorSchema, remoteMessageCases, remoteMessageSchema, remoteModelSchema, remove, removeLayer, removeOverlay, requirementKey, requirementsOf, retentionRootsSchema, schemaOf, segment, setStale, settleFailure, settleSuccess, shouldWake, stableStringify, targetsOf, terminal, tombstone, unknown, updateRemote, visibleItems, visibleStore, windowKey, writeEntities, writeEntity, writeRead };
2918
3145
 
2919
3146
  //# sourceMappingURL=index.mjs.map