foldkit-remote 0.6.0 → 0.7.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
@@ -1344,12 +1344,6 @@ const assembleRelation = (store, value, relation) => {
1344
1344
  * Entities: the descriptor an application declares, refs, ref pages, and
1345
1345
  * patches, with the codecs that carry refs on the wire.
1346
1346
  */
1347
- /**
1348
- * Relations are **references**, never inline target schemas: a ref cannot
1349
- * reconstruct a full entity, and dereferencing is a store concern. Because the
1350
- * target schema is not inlined, recursive relations cannot arise through the
1351
- * schema graph.
1352
- */
1353
1347
  const refCodec = (entity) => Schema.Struct({
1354
1348
  entity: Schema.String,
1355
1349
  id: Schema.String
@@ -2457,9 +2451,10 @@ const withoutQueryLoading = (loading, identity) => {
2457
2451
  * leaves its mark behind, and a connection released with that mark would read
2458
2452
  * `Loading` forever the next time something asked for it before sending.
2459
2453
  */
2460
- const prunedQueryLoading = (loading, roots) => {
2454
+ const prunedLoading = (loading, roots, reached) => {
2455
+ if (loading.size === 0) return loading;
2461
2456
  const named = new Set(roots.connections.map((root) => connectionMark(root.identity)));
2462
- const kept = [...loading].filter((mark) => !mark.startsWith(connectionPrefix) || named.has(mark));
2457
+ const kept = [...loading].filter((mark) => mark.startsWith(connectionPrefix) ? named.has(mark) : reached().has(markEntity(mark)));
2463
2458
  return kept.length === loading.size ? loading : new Set(kept);
2464
2459
  };
2465
2460
  /**
@@ -2589,12 +2584,14 @@ const updateRemote = (model, message) => {
2589
2584
  };
2590
2585
  case "RetentionChanged": {
2591
2586
  const retained = gc(model, message.roots);
2587
+ let reachedKeys;
2588
+ const reached = () => reachedKeys ??= reachable(model.entities, message.roots, model.connections, model.optimistic, model.mutations.pending);
2592
2589
  return {
2593
2590
  ...model,
2594
2591
  ...retained,
2595
2592
  refresh: prunedRefresh(model.refresh, retained),
2596
- failures: prunedFailures(model, message.roots),
2597
- loading: prunedQueryLoading(model.loading, message.roots)
2593
+ failures: prunedFailures(model.failures, message.roots, reached),
2594
+ loading: prunedLoading(model.loading, message.roots, reached)
2598
2595
  };
2599
2596
  }
2600
2597
  case "Hydrated": {
@@ -2606,9 +2603,11 @@ const updateRemote = (model, message) => {
2606
2603
  })),
2607
2604
  stale: true
2608
2605
  }]);
2606
+ const written = Object.entries(message.entities).flatMap(([key, entry]) => message.merge === "replace" || !(key in model.entities) ? [...entry.present].map((field) => `${key}\u0000${field}`) : []);
2609
2607
  return {
2610
2608
  ...model,
2611
2609
  entities: RemotePersistence.mergeStores(model.entities, message.entities, message.merge),
2610
+ failures: withoutFieldFailures(model.failures, written),
2612
2611
  connections: message.merge === "replace" ? {
2613
2612
  ...model.connections,
2614
2613
  ...Object.fromEntries(restored)
@@ -2621,7 +2620,7 @@ const updateRemote = (model, message) => {
2621
2620
  case "RefreshStarted": {
2622
2621
  const asked = message.requests.reduce((store, request) => isTombstone(store, entityKey(request.entity, request.id)) ? remove(store, entityKey(request.entity, request.id)) : store, model.entities);
2623
2622
  const entities = setStale(asked, marksOf(message.requests), true);
2624
- const failures = withoutFieldFailures(model.failures, fieldMarks(message.requests));
2623
+ const failures = withoutFieldFailures(model.failures, reachedMarks(model.entities, message.requests));
2625
2624
  return entities === model.entities && failures === model.failures ? model : {
2626
2625
  ...model,
2627
2626
  entities,
@@ -2737,7 +2736,6 @@ const updateRemote = (model, message) => {
2737
2736
  };
2738
2737
  case "QueryFailed": return {
2739
2738
  ...model,
2740
- connections: message.connection in model.connections ? setConnectionStale(model.connections, message.connection, false) : model.connections,
2741
2739
  failures: {
2742
2740
  ...model.failures,
2743
2741
  connections: {
@@ -2793,6 +2791,35 @@ const patchedMarks = function* (patches) {
2793
2791
  for (const patch of patches) for (const field of Object.keys(patch.values)) yield fieldMark(patch.entity, patch.id, field);
2794
2792
  };
2795
2793
  /**
2794
+ * Every field a read's requests reach: their own, and through each relation
2795
+ * the store already holds, the fields they ask of its targets. A request with
2796
+ * a relation riding on it names no target id; the store does, and a failure
2797
+ * recorded on a target is reached only this way.
2798
+ */
2799
+ const reachedMarks = (store, requests) => {
2800
+ const marks = [];
2801
+ const walked = /* @__PURE__ */ new Map();
2802
+ const walk = (entity, id, requirement) => {
2803
+ const key = entityKey(entity, id);
2804
+ const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
2805
+ if (seen.has(requirement)) return;
2806
+ seen.add(requirement);
2807
+ walked.set(key, seen);
2808
+ for (const field of requirement.fields) {
2809
+ marks.push(fieldMark(entity, id, field));
2810
+ const nested = requirement.relations?.[field];
2811
+ if (nested === void 0) continue;
2812
+ const value = readField(store, key, field);
2813
+ if (value._tag === "None") continue;
2814
+ for (const ref of targetsOf(value.value, nested)) walk(ref.entity, ref.id, nested);
2815
+ }
2816
+ };
2817
+ for (const request of requests) walk(request.entity, request.id, request);
2818
+ return marks;
2819
+ };
2820
+ /** The entity key a field mark belongs to. */
2821
+ const markEntity = (mark) => mark.slice(0, mark.indexOf("\0"));
2822
+ /**
2796
2823
  * The failures of what the roots still reach. A released connection's or
2797
2824
  * entity's failure goes with it, so asking for it again later asks the server
2798
2825
  * again rather than repeating an error that may no longer hold.
@@ -2802,12 +2829,10 @@ const patchedMarks = function* (patches) {
2802
2829
  * which is exactly a value whose first read failed: it is still on screen,
2803
2830
  * behind the parent that refers to it.
2804
2831
  */
2805
- const prunedFailures = (model, roots) => {
2806
- const { failures } = model;
2832
+ const prunedFailures = (failures, roots, reached) => {
2807
2833
  const named = new Set(roots.connections.map((root) => root.identity));
2808
2834
  const connections = Object.entries(failures.connections).filter(([identity]) => named.has(identity));
2809
- const kept = Object.keys(failures.fields).length === 0 ? /* @__PURE__ */ new Set() : reachable(model.entities, roots, model.connections, model.optimistic, model.mutations.pending);
2810
- const fields = Object.entries(failures.fields).filter(([mark]) => kept.has(mark.slice(0, mark.indexOf("\0"))));
2835
+ const fields = Object.keys(failures.fields).length === 0 ? [] : Object.entries(failures.fields).filter(([mark]) => reached().has(markEntity(mark)));
2811
2836
  return connections.length === Object.keys(failures.connections).length && fields.length === Object.keys(failures.fields).length ? failures : {
2812
2837
  connections: Object.fromEntries(connections),
2813
2838
  fields: Object.fromEntries(fields)
@@ -2816,6 +2841,32 @@ const prunedFailures = (model, roots) => {
2816
2841
  /** Whether this field's last read failed and nothing has settled it since. */
2817
2842
  const isFieldFailed = (model, entity, id, field) => fieldMark(entity, id, field) in model.failures.fields;
2818
2843
  /**
2844
+ * Whether a read is fetching any field `relation` reads of an entity, its own
2845
+ * or, through each relation the store holds, its targets'. A value whose owner
2846
+ * is being fetched is loading, not `Initial`: nothing is missing a fetch.
2847
+ */
2848
+ const isLoadingThrough = (model, entity, id, relation) => {
2849
+ if (model.loading.size === 0) return false;
2850
+ const walked = /* @__PURE__ */ new Map();
2851
+ const walk = (target, targetId, requirement) => {
2852
+ const key = entityKey(target, targetId);
2853
+ const seen = walked.get(key) ?? /* @__PURE__ */ new Set();
2854
+ if (seen.has(requirement)) return false;
2855
+ seen.add(requirement);
2856
+ walked.set(key, seen);
2857
+ for (const field of requirement.fields) {
2858
+ if (model.loading.has(fieldMark(target, targetId, field))) return true;
2859
+ const nested = requirement.relations?.[field];
2860
+ if (nested === void 0) continue;
2861
+ const value = readField(model.entities, key, field);
2862
+ if (value._tag === "None") continue;
2863
+ for (const ref of targetsOf(value.value, nested)) if (walk(ref.entity, ref.id, nested)) return true;
2864
+ }
2865
+ return false;
2866
+ };
2867
+ return walk(entity, id, relation);
2868
+ };
2869
+ /**
2819
2870
  * The first failure among the fields `relation` reads of an entity, following
2820
2871
  * each relation the store holds into its targets, as `assemble` does. A read
2821
2872
  * that consults it shows `Failed` for a related entity's failure too, rather
@@ -3077,6 +3128,26 @@ const askedOf = (projection) => projection === void 0 ? nothingAsked : {
3077
3128
  connections: connectionsOf(projection)
3078
3129
  };
3079
3130
  /**
3131
+ * Whether `outer` asks for everything `inner` does of one entity: each field,
3132
+ * each relation's own slice of its target, and each page window alike.
3133
+ */
3134
+ const sliceCovers = (outer, inner) => outer.entity === inner.entity && inner.fields.every((field) => outer.fields.includes(field)) && Object.entries(inner.relations ?? {}).every(([field, relation]) => {
3135
+ const wider = outer.relations?.[field];
3136
+ return wider !== void 0 && sliceCovers(wider, relation);
3137
+ }) && Object.entries(inner.windows ?? {}).every(([field, window]) => outer.windows?.[field] !== void 0 && stableStringify(outer.windows[field]) === stableStringify(window));
3138
+ /**
3139
+ * Whether what `outer` asks includes everything `inner` asks: each list with
3140
+ * the slice it selects of its rows, and each entity with the slice it reads,
3141
+ * relations included. A Surface that shows a read is one whose projection
3142
+ * asks for all of it; one that reads part of it cannot be why it is fetched.
3143
+ * A Surface that asks for one entity in several pieces is already one
3144
+ * requirement here: projection metadata unions them per entity and id.
3145
+ */
3146
+ const covers = (outer, inner) => {
3147
+ const { connections, requirements } = outer;
3148
+ return inner.connections.every((connection) => connections.some((wider) => wider.identity === connection.identity && (connection.select === void 0 || wider.select !== void 0 && sliceCovers(wider.select, connection.select)))) && inner.requirements.every((requirement) => requirements.some((wider) => wider.id === requirement.id && sliceCovers(wider, requirement)));
3149
+ };
3150
+ /**
3080
3151
  * Plans what a projection asks against the Model. A connection the Model holds
3081
3152
  * fresh contributes its visible items' selected fields to the entity plan; one
3082
3153
  * it does not hold, or holds stale, is a query to run (its items are planned
@@ -3369,6 +3440,21 @@ const Remote = {
3369
3440
  /** Whether a Message is one of Remote's, by tag. */
3370
3441
  reduces: isRemoteMessage,
3371
3442
  /**
3443
+ * A patch of one entity's values, for a mutation's `optimistic` list or an
3444
+ * overlay. Takes a `foldkit-entity` Entity or a Remote descriptor alike, so
3445
+ * the domain you declared once is the one you patch:
3446
+ * `Remote.patch(Project, 'p1', { name: 'Apollo II' })`. Values are the
3447
+ * fields' wire shape, as the server writes them; a relation is its ref key
3448
+ * (`'User:u1'`).
3449
+ */
3450
+ patch: (entity, id, values) => descriptorOf(entity).patch(id, values),
3451
+ /**
3452
+ * A reference to one entity, for `ConnectionChange` and anything else that
3453
+ * names a row: `Remote.ref(Project, 'p3')`. Either kind of entity, as with
3454
+ * `Remote.patch`.
3455
+ */
3456
+ ref: (entity, id) => descriptorOf(entity).ref(id),
3457
+ /**
3372
3458
  * Declares a Remote domain and binds it to its place in the application Model
3373
3459
  * in one step; the result carries the application-facing operations. The
3374
3460
  * descriptor alone is `Remote.define`, the binding alone `Remote.at`.
@@ -3433,7 +3519,7 @@ const Remote = {
3433
3519
  error: failure,
3434
3520
  previous: present.value
3435
3521
  } : present;
3436
- if (isLoading(remote, selection.entity, id, relation.fields)) return { _tag: "Loading" };
3522
+ if (isLoadingThrough(remote, selection.entity, id, relation)) return { _tag: "Loading" };
3437
3523
  return failure === void 0 ? { _tag: "Initial" } : {
3438
3524
  _tag: "Failed",
3439
3525
  error: failure
@@ -3795,7 +3881,7 @@ const bindDomain = (definition, store) => {
3795
3881
  if (remote.loading.size === 0) return void 0;
3796
3882
  for (const edge of visibleItems(connection, ref.identity, remote.optimistic.overlays, remote.entities)) {
3797
3883
  if (edge.ref.entity !== relation.entity) continue;
3798
- if (isLoading(remote, edge.ref.entity, edge.ref.id, relation.fields)) return { _tag: "Loading" };
3884
+ if (isLoadingThrough(remote, edge.ref.entity, edge.ref.id, relation)) return { _tag: "Loading" };
3799
3885
  }
3800
3886
  };
3801
3887
  const readPage = (remote, connection) => {
@@ -3931,6 +4017,59 @@ const bindDomain = (definition, store) => {
3931
4017
  }
3932
4018
  };
3933
4019
  },
4020
+ why: (model, projection, options) => {
4021
+ const state = projection.read(model);
4022
+ const asked = askedOf(projection);
4023
+ const reading = options?.surfaces === void 0 ? void 0 : Object.values(options.surfaces).filter((active) => {
4024
+ const shown = active.projectionOf(model);
4025
+ return shown !== void 0 && covers(askedOf(shown), asked);
4026
+ }).map((active) => active.name);
4027
+ const withSurfaces = reading === void 0 ? {} : { surfaces: reading };
4028
+ switch (state._tag) {
4029
+ case "Ready": return {
4030
+ state: state._tag,
4031
+ message: "Everything it selects is here.",
4032
+ ...withSurfaces
4033
+ };
4034
+ case "Refreshing": return {
4035
+ state: state._tag,
4036
+ message: "It is shown while a newer value is fetched.",
4037
+ ...withSurfaces
4038
+ };
4039
+ case "Loading": return {
4040
+ state: state._tag,
4041
+ message: "A request for it is in flight.",
4042
+ ...withSurfaces
4043
+ };
4044
+ case "NotFound": return {
4045
+ state: state._tag,
4046
+ message: "The server answered without it: it does not exist, or this principal may not see it.",
4047
+ ...withSurfaces
4048
+ };
4049
+ case "Failed": return {
4050
+ state: state._tag,
4051
+ message: state.error._tag === "DecodeError" ? `What the server sent does not decode against the Selection: ${state.error.message}` : `Its request failed: ${state.error.message}. Nothing retries a failed read on its own; Data.refresh asks again.`,
4052
+ ...withSurfaces
4053
+ };
4054
+ case "Initial":
4055
+ if (reading === void 0) return {
4056
+ state: state._tag,
4057
+ reason: "Unknown",
4058
+ message: "Nothing is fetching it. Pass the active record you give Data.subscriptions as `surfaces` to tell whether an active Surface reads it."
4059
+ };
4060
+ return reading.length === 0 ? {
4061
+ state: state._tag,
4062
+ reason: "NotObserved",
4063
+ message: "No active Surface reads it, so nothing fetches it. Include it in a Surface that is active for this Model, in the record given to Data.subscriptions.",
4064
+ surfaces: reading
4065
+ } : {
4066
+ state: state._tag,
4067
+ reason: "NotFetching",
4068
+ message: `${reading.join(", ")} ${reading.length === 1 ? "reads it and is" : "read it and are"} active, yet nothing is fetching it. Remote's Subscriptions are most likely not installed: give the runtime Data.subscriptions (or Data.wiring) and a RemoteClient.`,
4069
+ surfaces: reading
4070
+ };
4071
+ }
4072
+ },
3934
4073
  filtered: (model, over, by, input) => {
3935
4074
  assertRegistered(bound, "Query", definition.registry.queries, by.name);
3936
4075
  const filters = by.body?.entity.name;
@@ -4054,6 +4193,6 @@ const pageSize = (window, key) => {
4054
4193
  return size === void 0 ? {} : { [key]: size };
4055
4194
  };
4056
4195
  //#endregion
4057
- export { ConnectionChange, ConnectionChangeSchema, Entity, IDENTITY_SEPARATOR, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_RELATION_DEPTH, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, PageSize, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, RELATION_ALIAS, 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, aliasedField, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, belongsEncoded, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyRefresh, emptyStore, entityKey, entry, failMutation, failureOf, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote, isFieldFailed, isFieldStale, isGapped, isLoading, isQueryLoading, isRefPage, isRemoteMessage, isTombstone, items, liveEventOf, liveHasNext, liveHasPrevious, markStale, matching, merge, missingFields, mutateRemote, mutationStatus, noFailures, pageSchema, plan, pruneOverlays, reachable, readField, reconcileMutation, refParts, refreshIsInFlight, refreshedAt, refsIn, relationAlias, 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, withRefreshRequested, writeEntities, writeEntity, writeRead };
4196
+ export { ConnectionChange, ConnectionChangeSchema, Entity, IDENTITY_SEPARATOR, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_RELATION_DEPTH, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, PageSize, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, RELATION_ALIAS, 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, aliasedField, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, belongsEncoded, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyRefresh, emptyStore, entityKey, entry, failMutation, failureOf, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote, isFieldFailed, isFieldStale, isGapped, isLoading, isLoadingThrough, isQueryLoading, isRefPage, isRemoteMessage, isTombstone, items, liveEventOf, liveHasNext, liveHasPrevious, markStale, matching, merge, missingFields, mutateRemote, mutationStatus, noFailures, pageSchema, plan, pruneOverlays, reachable, readField, reconcileMutation, refParts, refreshIsInFlight, refreshedAt, refsIn, relationAlias, 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, withRefreshRequested, writeEntities, writeEntity, writeRead };
4058
4197
 
4059
4198
  //# sourceMappingURL=index.mjs.map