foldkit-remote 0.18.1 → 0.19.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.
@@ -420,6 +420,7 @@ const emptyMutationState = {
420
420
  applied: /* @__PURE__ */ new Set(),
421
421
  failed: /* @__PURE__ */ new Set(),
422
422
  errors: /* @__PURE__ */ new Map(),
423
+ operations: /* @__PURE__ */ new Map(),
423
424
  sequence: 0
424
425
  };
425
426
  /**
@@ -442,17 +443,20 @@ const beginMutation = (state, requestId) => ({
442
443
  pending: /* @__PURE__ */ new Set([...state.pending, requestId]),
443
444
  sequence: state.sequence + 1
444
445
  });
445
- const failMutation = (state, requestId, error) => {
446
+ const failMutation = (state, requestId, error, operation) => {
446
447
  const pending = new Set(state.pending);
447
448
  pending.delete(requestId);
448
449
  const failed = remember(state.failed, requestId);
449
450
  const errors = new Map([...state.errors, [requestId, error]]);
451
+ const operations = new Map([...state.operations, [requestId, operation]]);
450
452
  for (const id of errors.keys()) if (!failed.has(id)) errors.delete(id);
453
+ for (const id of operations.keys()) if (!failed.has(id)) operations.delete(id);
451
454
  return {
452
455
  ...state,
453
456
  pending,
454
457
  failed,
455
- errors
458
+ errors,
459
+ operations
456
460
  };
457
461
  };
458
462
  const mutationStatus = (state, requestId) => {
@@ -487,6 +491,23 @@ const reconcileMutation = (store, state, requestId, answer) => {
487
491
  };
488
492
  };
489
493
  const conflict = Schema.TaggedStruct("Conflict", {});
494
+ const unmet = Schema.TaggedStruct("Unmet", {});
495
+ /**
496
+ * The row the write would leave breaks one of its Entity's invariants: which
497
+ * one, what to tell the author, and the fields to show it beside. None and
498
+ * empty when the invariant reads a member hidden from whoever asked.
499
+ */
500
+ const invalid = Schema.TaggedStruct("Invalid", {
501
+ rule: Schema.OptionFromNullOr(Schema.String),
502
+ message: Schema.String,
503
+ path: Schema.Array(Schema.String)
504
+ });
505
+ const conflictOrInvalid = Schema.Union([conflict, invalid]);
506
+ const conflictUnmetOrInvalid = Schema.Union([
507
+ conflict,
508
+ unmet,
509
+ invalid
510
+ ]);
490
511
  const anyField = Schema.TaggedStruct("Field", {
491
512
  key: Schema.String,
492
513
  reason: Schema.Unknown
@@ -500,10 +521,24 @@ const Refusal = {
500
521
  }),
501
522
  /** The row moved on since the client read it; the author decides what wins. */
502
523
  conflict,
524
+ /** The row does not hold what the write's `when` asks of it, where it would commit. */
525
+ unmet,
526
+ /** The row the write would leave breaks an invariant of its Entity. */
527
+ invalid,
503
528
  /** Whether a refusal, of whatever mutation, is a `Refusal.field`. */
504
529
  isField: Schema.is(anyField),
505
530
  /** Whether a refusal, of whatever mutation, is `Refusal.conflict`. */
506
- isConflict: Schema.is(conflict)
531
+ isConflict: Schema.is(conflict),
532
+ /** Whether a refusal, of whatever mutation, is `Refusal.unmet`. */
533
+ isUnmet: Schema.is(unmet),
534
+ /** Whether a refusal, of whatever mutation, is `Refusal.invalid`. */
535
+ isInvalid: Schema.is(invalid),
536
+ /**
537
+ * Whether a refusal is `Refusal.invalid` naming this invariant: the one
538
+ * `Entity.invariant` returned, or one `Entity.invariants` reads. An invariant
539
+ * over a member hidden from the asker names none, so it matches nothing.
540
+ */
541
+ isInvalidBy: (refusal, invariant) => Schema.is(invalid)(refusal) && Option.contains(refusal.rule, invariant.id)
507
542
  };
508
543
  /** The codec itself, or a `Schema.Struct` over the fields. */
509
544
  const schemaOf = (shape) => Schema.isSchema(shape) ? shape : Schema.Struct(shape);
@@ -525,11 +560,18 @@ const patchOfWrite = (bound) => ({
525
560
  /**
526
561
  * What a declared write shows before the server answers: an update's or an
527
562
  * insert's row as a patch (an update's only for the keys named, when some
528
- * are). A delete shows nothing, since no operation removes a row from every
563
+ * are), and an update's increments as amounts over the value the store holds. A delete shows nothing, since no operation removes a row from every
529
564
  * list it is in; its answer does.
530
565
  */
531
566
  const optimisticOfWrite = (write, input, keys) => Match.value(write).pipe(Match.tagsExhaustive({
532
- Update: () => [patchOfWrite(Write.bind(write, input, keys))],
567
+ Update: () => {
568
+ const bound = Write.bind(write, input, keys);
569
+ const patch = patchOfWrite(bound);
570
+ return [Object.keys(bound.increments).length === 0 ? patch : {
571
+ ...patch,
572
+ adds: bound.increments
573
+ }];
574
+ },
533
575
  Insert: () => {
534
576
  const patch = patchOfWrite(Write.bind(write, input));
535
577
  return [{
@@ -553,13 +595,14 @@ const Mutation = {
553
595
  * A mutation that is a declared `Write` (`update`, `insert` or `delete`): its
554
596
  * input is the write's, and it answers nothing but the row it changed, as a
555
597
  * patch, or its deletion. It may be refused as a conflict, for a write that
556
- * `expect`s a revision the row has moved past.
598
+ * `expect`s a revision the row has moved past, for a write with a `when` as
599
+ * unmet, and as invalid when the row it would leave breaks an invariant.
557
600
  */
558
601
  write: (name, write) => ({
559
602
  name,
560
603
  Input: write.input.schema,
561
604
  Output: Schema.Struct({}),
562
- Refusal: Refusal.conflict,
605
+ Refusal: Option.isSome(write.when) ? conflictUnmetOrInvalid : conflictOrInvalid,
563
606
  write
564
607
  }),
565
608
  make: (name, config) => ({
@@ -822,9 +865,21 @@ const ReadBatchResult = Schema.Struct({
822
865
  entities: Schema.Array(NormalizedEntity),
823
866
  settled: Schema.Array(SettledFields)
824
867
  });
868
+ /**
869
+ * One operation's identity across every client: minted where it is first sent
870
+ * and sent again with each retry of it. Bounded, since a server keys a record
871
+ * of the write by it.
872
+ */
873
+ const OperationId = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(128), Schema.isPattern(/^[A-Za-z0-9:._-]+$/));
825
874
  const MutationRequest = Schema.Struct({
826
- /** Stable across transport retries so a mutation is not applied twice. */
875
+ /**
876
+ * This client's id for the request, the same on each retry of it. It is
877
+ * unique within one client's Model only (a counter there), so it names a
878
+ * request to that client; `operation` names it to a server.
879
+ */
827
880
  requestId: Schema.String,
881
+ /** The operation's identity across clients: what a server keys a record of the write by. */
882
+ operation: Schema.optional(OperationId),
828
883
  mutation: Schema.String,
829
884
  input: Schema.Unknown,
830
885
  /** For a mutation that is a `Write`: the input keys to write, when not all of them. */
@@ -1198,18 +1253,28 @@ const coalescedLayer = (layer, options = {}) => Layer.effect(RemoteClient, Effec
1198
1253
  };
1199
1254
  })).pipe(Layer.provide(layer));
1200
1255
  /**
1256
+ * A new operation's identity: random, so unique across clients and reloads.
1257
+ * `randomUUID` exists only in a secure context, so a page served over plain
1258
+ * HTTP draws 128 random bits from `getRandomValues`, which exists in all.
1259
+ */
1260
+ const mintOperation = () => typeof globalThis.crypto.randomUUID === "function" ? globalThis.crypto.randomUUID() : Array.from(globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, "0")).join("");
1261
+ /**
1201
1262
  * `Remote.mutate` as a standalone effect, so `Remote.mutateInto` can reuse it
1202
1263
  * without the object literal referencing itself.
1203
1264
  */
1204
- const mutateRemote = Effect.fn("Remote.mutate")(function* (mutation, input, requestId, keys) {
1265
+ const mutateRemote = Effect.fn("Remote.mutate")(function* (mutation, input, requestId, options = {}) {
1266
+ const { keys } = options;
1267
+ const operation = options.operation ?? (yield* Effect.sync(mintOperation));
1205
1268
  yield* Effect.annotateCurrentSpan({
1206
1269
  mutation: mutation.name,
1207
- requestId
1270
+ requestId,
1271
+ operation
1208
1272
  });
1209
1273
  const client = yield* RemoteClient;
1210
1274
  const encoded = yield* Schema.encodeUnknownEffect(mutation.Input)(input).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new RemoteMutationError({ message: error.message }))));
1211
1275
  const result = yield* client.mutate({
1212
1276
  requestId,
1277
+ operation,
1213
1278
  mutation: mutation.name,
1214
1279
  input: encoded,
1215
1280
  ...keys === void 0 ? {} : { keys }
@@ -2137,7 +2202,7 @@ const removeOverlay = (optimistic, id) => {
2137
2202
  * entity only a request holds (a temporary id) is not the server's to age, and
2138
2203
  * is dated `Infinity`, so freshness never plans or times it.
2139
2204
  */
2140
- const visibleStore = (base, optimistic) => writeEntities(base, optimistic.layers.flatMap((layer) => layer.patches.map((patch) => {
2205
+ const visibleStore = (base, optimistic) => writeEntities(base, resolvedPatches(base, optimistic).map((patch) => {
2141
2206
  const key = entityKey(patch.entity, patch.id);
2142
2207
  const under = base[key];
2143
2208
  return {
@@ -2145,7 +2210,34 @@ const visibleStore = (base, optimistic) => writeEntities(base, optimistic.layers
2145
2210
  values: patch.values,
2146
2211
  updatedAt: under === void 0 || under.tombstone ? Infinity : under.updatedAt
2147
2212
  };
2148
- })));
2213
+ }));
2214
+ /**
2215
+ * Every pending patch in order, each amount added to the value under it: the
2216
+ * store's, or an earlier layer's. A prediction, so only over a number the
2217
+ * client holds: a field it has not read, or holds as anything else, keeps
2218
+ * what it shows until the server answers with the sum.
2219
+ */
2220
+ const resolvedPatches = (base, optimistic) => {
2221
+ const shown = /* @__PURE__ */ new Map();
2222
+ return optimistic.layers.flatMap((layer) => layer.patches.map(({ adds, ...patch }) => {
2223
+ const key = entityKey(patch.entity, patch.id);
2224
+ const earlier = shown.get(key) ?? {};
2225
+ const values = { ...patch.values };
2226
+ const under = base[key];
2227
+ const valueUnder = (field) => Object.hasOwn(earlier, field) ? Option.some(earlier[field]) : Option.fromUndefinedOr(under?.values[field]);
2228
+ for (const [field, amount] of Object.entries(adds ?? {})) Option.map(Option.filter(valueUnder(field), Predicate.isNumber), (current) => {
2229
+ values[field] = current + amount;
2230
+ });
2231
+ shown.set(key, {
2232
+ ...earlier,
2233
+ ...values
2234
+ });
2235
+ return {
2236
+ ...patch,
2237
+ values
2238
+ };
2239
+ }));
2240
+ };
2149
2241
  /**
2150
2242
  * A fresh page is newer than any settled overlay it covers: an edge the page
2151
2243
  * carries no longer needs an insert overlay, and a `remove` overlay hiding it
@@ -2991,7 +3083,8 @@ const remoteMessageCases = {
2991
3083
  },
2992
3084
  MutationFailed: {
2993
3085
  requestId: Schema.String,
2994
- error: remoteErrorSchema
3086
+ error: remoteErrorSchema,
3087
+ operation: Schema.String
2995
3088
  },
2996
3089
  OverlayShown: {
2997
3090
  id: Schema.String,
@@ -3432,7 +3525,7 @@ const reduceRemote = (model, message) => {
3432
3525
  }
3433
3526
  case "MutationFailed": return {
3434
3527
  ...model,
3435
- mutations: failMutation(model.mutations, message.requestId, message.error),
3528
+ mutations: failMutation(model.mutations, message.requestId, message.error, message.operation),
3436
3529
  optimistic: settleFailure(model.optimistic, message.requestId)
3437
3530
  };
3438
3531
  case "LiveReceived": {
@@ -5492,7 +5585,7 @@ const bindDomain = (definition, store, options = {}) => {
5492
5585
  pages,
5493
5586
  layers,
5494
5587
  remote.entities
5495
- ], `placed:${identity}`, () => placedHeld(identity, pages, changesOf({ entities: layers.flatMap((layer) => layer.patches) }, remote.entities), visibleStoreOf(remote.entities, remote.optimistic)));
5588
+ ], `placed:${identity}`, () => placedHeld(identity, pages, changesOf({ entities: resolvedPatches(remote.entities, remote.optimistic) }, remote.entities), visibleStoreOf(remote.entities, remote.optimistic)));
5496
5589
  };
5497
5590
  /**
5498
5591
  * The edges a loaded list shows: its pages with pending edits placed, and
@@ -6071,6 +6164,7 @@ const bindDomain = (definition, store, options = {}) => {
6071
6164
  const remote = store.get(model);
6072
6165
  const requestId = options.requestId ?? `${bound.contract.name}-${remote.mutations.sequence + 1}`;
6073
6166
  const tempId = `${requestId}.tmp`;
6167
+ const known = Option.fromUndefinedOr(remote.mutations.operations.get(requestId));
6074
6168
  const optimistic = typeof options.optimistic === "function" ? options.optimistic({
6075
6169
  requestId,
6076
6170
  tempId
@@ -6087,11 +6181,15 @@ const bindDomain = (definition, store, options = {}) => {
6087
6181
  command: {
6088
6182
  name: `Remote.mutate(${mutation.name})`,
6089
6183
  args: { requestId },
6090
- effect: mutateRemote(mutation, input, requestId, options.keys).pipe(Effect.match({
6184
+ effect: Effect.flatMap(Effect.sync(() => Option.getOrElse(known, mintOperation)), (operation) => mutateRemote(mutation, input, requestId, {
6185
+ keys: options.keys,
6186
+ operation
6187
+ }).pipe(Effect.match({
6091
6188
  onFailure: (error) => ({
6092
6189
  _tag: "MutationFailed",
6093
6190
  requestId,
6094
- error: remoteError(error)
6191
+ error: remoteError(error),
6192
+ operation
6095
6193
  }),
6096
6194
  onSuccess: (outcome) => ({
6097
6195
  _tag: "MutationSucceeded",
@@ -6101,7 +6199,7 @@ const bindDomain = (definition, store, options = {}) => {
6101
6199
  deleted: outcome.deleted,
6102
6200
  now: (options.now ?? wallClock)()
6103
6201
  })
6104
- }))
6202
+ })))
6105
6203
  }
6106
6204
  };
6107
6205
  },
@@ -6254,6 +6352,6 @@ const pickWindow = (window) => ({
6254
6352
  ...window.before === void 0 ? {} : { before: window.before }
6255
6353
  });
6256
6354
  //#endregion
6257
- export { emptyOptimistic as $, refParts as $n, QueryResult as $t, writeRead as A, schemaOf as An, RemoteClient as At, plan as B, missingFields as Bn, Live as Bt, remoteMessageCases as C, beginMutation as Cn, hasPrevious as Ct, updateRemote as D, optimisticOfWrite as Dn, segment as Dt, retentionRootsSchema as E, mutationStatus as En, merge as Et, REMOTE_CACHE_VERSION as F, hasField as Fn, remoteError as Ft, emptyLiveState as G, tombstone as Gn, MAX_RELATION_DEPTH as Gt, applyConnectionEvent as H, remove as Hn, LiveEdge as Ht, RemotePersistence as I, isFieldStale as In, coalesceQueries as It, ConnectionChange as J, RELATION_ALIAS as Jn, MutationResult as Jt, liveHasNext as K, writeEntities as Kn, Mutate as Kt, gc as L, isFieldUnavailable as Ln, coalesceReads as Lt, RemoteData as M, emptyStore as Mn, connectionChangeOf as Mt, remoteDataSchema as N, entityKey as Nn, liveEventOf as Nt, windowGrowthKey as O, patchOfWrite as On, terminal as Ot, remoteErrorSchema as P, entry as Pn, mutateRemote as Pt, connectionIdentity as Q, isRefPage as Qn, QueryRequest as Qt, reachable as R, isTombstone as Rn, requirementKey as Rt, refreshedAt as S, Refusal as Sn, hasNext as St, remoteModelSchema as T, failMutation as Tn, items as Tt, applyEntityEvent as U, setStale as Un, LiveRequirement as Ut, windowKey as V, readField as Vn, LiveChange as Vt, classifyLive as W, setUnavailable as Wn, MAX_FIELDS_PER_REQUEST as Wt, addOverlay as X, RelationEntityAnnotation as Xn, PageSize as Xt, addLayer as Y, RelationAnnotation as Yn, NormalizedEntity as Yt, beginOptimistic as Z, aliasedField as Zn, QueryEdge as Zt, isRemoteMessage as _, WireBoundary as _n, relationOf as _t, RemoteJsonAnswer as a, ReadRequest as an, RemoteRequirements as ar, visibleItems as at, overlaysOf as b, stableStringify as bn, edge as bt, emptyRefresh as c, RemoteLiveError as cn, requirementsOf as cr, inspectRemote as ct, initialRemoteModel as d, RemoteQueryError as dn, RowPlace as dt, QueryRpc as en, refsIn as er, pruneOverlays as et, invalidateConnections as f, RemoteReadError as fn, placeIn as ft, isQueryLoading as g, WindowSchema as gn, pageSchema as gt, isLoadingThrough as h, SettledFields as hn, assemble as ht, liveFetch as i, ReadBatchResult as in, RemoteConnections as ir, settleSuccess as it, Denied as j, clearStale as jn, coalescedLayer as jt, withRefreshRequested as k, reconcileMutation as kn, unknown as kt, failureOf as l, RemoteMutationError as ln, belongsEncoded as lt, isLoading as m, RemoteUnauthenticated as mn, Selection as mt, RemoteUnsatisfied as n, Read as nn, relationShape as nr, removeOverlay as nt, RemoteJsonRequest as o, RelationRequest as on, Requirement as or, visibleStore as ot, isFieldFailed as p, RemoteRpc as pn, Entity$1 as pt, liveHasPrevious as q, writeEntity as qn, MutationRequest as qt, httpWithLive as r, ReadBatch as rn, targetsOf as rr, settleFailure as rt, RemotePolicy as s, RemoteForbidden as sn, connectionsOf as sr, inspectEntity as st, Remote as t, REMOTE_PROTOCOL_VERSION as tn, relationAlias as tr, removeLayer as tt, forgetRemote as u, RemoteProtocolError as un, matching as ut, noFailures as v, IDENTITY_SEPARATOR as vn, unavailableOf as vt, remoteMessageSchema as w, emptyMutationState as wn, isGapped as wt, refreshIsInFlight as x, Mutation as xn, emptyConnection as xt, onlyOverlaid as y, Query$1 as yn, cursor as yt, deadlineOf as z, markStale as zn, ConnectionChangeSchema as zt };
6355
+ export { emptyOptimistic as $, RelationEntityAnnotation as $n, PageSize as $t, writeRead as A, optimisticOfWrite as An, unknown as At, plan as B, isFieldUnavailable as Bn, requirementKey as Bt, remoteMessageCases as C, stableStringify as Cn, hasNext as Ct, updateRemote as D, emptyMutationState as Dn, merge as Dt, retentionRootsSchema as E, beginMutation as En, items as Et, REMOTE_CACHE_VERSION as F, emptyStore as Fn, mintOperation as Ft, emptyLiveState as G, remove as Gn, LiveRequirement as Gt, applyConnectionEvent as H, markStale as Hn, Live as Ht, RemotePersistence as I, entityKey as In, mutateRemote as It, ConnectionChange as J, tombstone as Jn, Mutate as Jt, liveHasNext as K, setStale as Kn, MAX_FIELDS_PER_REQUEST as Kt, gc as L, entry as Ln, remoteError as Lt, RemoteData as M, reconcileMutation as Mn, coalescedLayer as Mt, remoteDataSchema as N, schemaOf as Nn, connectionChangeOf as Nt, windowGrowthKey as O, failMutation as On, segment as Ot, remoteErrorSchema as P, clearStale as Pn, liveEventOf as Pt, connectionIdentity as Q, RelationAnnotation as Qn, OperationId as Qt, reachable as R, hasField as Rn, coalesceQueries as Rt, refreshedAt as S, Query$1 as Sn, emptyConnection as St, remoteModelSchema as T, Refusal as Tn, isGapped as Tt, applyEntityEvent as U, missingFields as Un, LiveChange as Ut, windowKey as V, isTombstone as Vn, ConnectionChangeSchema as Vt, classifyLive as W, readField as Wn, LiveEdge as Wt, addOverlay as X, writeEntity as Xn, MutationResult as Xt, addLayer as Y, writeEntities as Yn, MutationRequest as Yt, beginOptimistic as Z, RELATION_ALIAS as Zn, NormalizedEntity as Zt, isRemoteMessage as _, RemoteUnauthenticated as _n, pageSchema as _t, RemoteJsonAnswer as a, Read as an, relationShape as ar, settleSuccess as at, overlaysOf as b, WireBoundary as bn, cursor as bt, emptyRefresh as c, ReadRequest as cn, RemoteRequirements as cr, inspectEntity as ct, initialRemoteModel as d, RemoteLiveError as dn, requirementsOf as dr, matching as dt, QueryEdge as en, aliasedField as er, pruneOverlays as et, invalidateConnections as f, RemoteMutationError as fn, RowPlace as ft, isQueryLoading as g, RemoteRpc as gn, assemble as gt, isLoadingThrough as h, RemoteReadError as hn, Selection as ht, liveFetch as i, REMOTE_PROTOCOL_VERSION as in, relationAlias as ir, settleFailure as it, Denied as j, patchOfWrite as jn, RemoteClient as jt, withRefreshRequested as k, mutationStatus as kn, terminal as kt, failureOf as l, RelationRequest as ln, Requirement as lr, inspectRemote as lt, isLoading as m, RemoteQueryError as mn, Entity$1 as mt, RemoteUnsatisfied as n, QueryResult as nn, refParts as nr, removeOverlay as nt, RemoteJsonRequest as o, ReadBatch as on, targetsOf as or, visibleItems as ot, isFieldFailed as p, RemoteProtocolError as pn, placeIn as pt, liveHasPrevious as q, setUnavailable as qn, MAX_RELATION_DEPTH as qt, httpWithLive as r, QueryRpc as rn, refsIn as rr, resolvedPatches as rt, RemotePolicy as s, ReadBatchResult as sn, RemoteConnections as sr, visibleStore as st, Remote as t, QueryRequest as tn, isRefPage as tr, removeLayer as tt, forgetRemote as u, RemoteForbidden as un, connectionsOf as ur, belongsEncoded as ut, noFailures as v, SettledFields as vn, relationOf as vt, remoteMessageSchema as w, Mutation as wn, hasPrevious as wt, refreshIsInFlight as x, IDENTITY_SEPARATOR as xn, edge as xt, onlyOverlaid as y, WindowSchema as yn, unavailableOf as yt, deadlineOf as z, isFieldStale as zn, coalesceReads as zt };
6258
6356
 
6259
- //# sourceMappingURL=src-Cp71fJac.mjs.map
6357
+ //# sourceMappingURL=src-fsYu0YoT.mjs.map