foldkit-remote 0.20.0 → 0.22.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/README.md +987 -198
- package/dist/answerGaps.d.ts +16 -0
- package/dist/answerGaps.d.ts.map +1 -0
- package/dist/answerGaps.js +60 -0
- package/dist/answerGaps.js.map +1 -0
- package/dist/client-B055kQuP.d.ts +110 -0
- package/dist/client-B055kQuP.d.ts.map +1 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +142 -0
- package/dist/client.js.map +1 -0
- package/dist/coalesce-CLAr92Nk.d.ts +34 -0
- package/dist/coalesce-CLAr92Nk.d.ts.map +1 -0
- package/dist/coalesce.d.ts +2 -0
- package/dist/coalesce.js +131 -0
- package/dist/coalesce.js.map +1 -0
- package/dist/connection-BRyaJy89.d.ts +45 -0
- package/dist/connection-BRyaJy89.d.ts.map +1 -0
- package/dist/connection.d.ts +2 -0
- package/dist/connection.js +170 -0
- package/dist/connection.js.map +1 -0
- package/dist/data.d.ts +7 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +21 -0
- package/dist/data.js.map +1 -0
- package/dist/diagnostics-DHhVYflV.d.ts +125 -0
- package/dist/diagnostics-DHhVYflV.d.ts.map +1 -0
- package/dist/diagnostics.d.ts +2 -0
- package/dist/diagnostics.js +60 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/entity-CgsG5Uel.d.ts +196 -0
- package/dist/entity-CgsG5Uel.d.ts.map +1 -0
- package/dist/entity-DuDxaa3n.js +279 -0
- package/dist/entity-DuDxaa3n.js.map +1 -0
- package/dist/entity.d.ts +2 -0
- package/dist/entity.js +1 -0
- package/dist/held.d.ts +19 -0
- package/dist/held.d.ts.map +1 -0
- package/dist/held.js +70 -0
- package/dist/held.js.map +1 -0
- package/dist/impact.d.ts +79 -0
- package/dist/impact.d.ts.map +1 -0
- package/dist/impact.js +166 -0
- package/dist/impact.js.map +1 -0
- package/dist/index.d.ts +1542 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2791 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect-DtRQGCRP.d.ts +134 -0
- package/dist/inspect-DtRQGCRP.d.ts.map +1 -0
- package/dist/inspect.d.ts +2 -0
- package/dist/inspect.js +33 -0
- package/dist/inspect.js.map +1 -0
- package/dist/json-DowLl1eB.d.ts +52 -0
- package/dist/json-DowLl1eB.d.ts.map +1 -0
- package/dist/json.d.ts +2 -0
- package/dist/json.js +123 -0
- package/dist/json.js.map +1 -0
- package/dist/law.d.ts +56 -0
- package/dist/law.d.ts.map +1 -0
- package/dist/law.js +201 -0
- package/dist/law.js.map +1 -0
- package/dist/live-BpTzlCnY.d.ts +79 -0
- package/dist/live-BpTzlCnY.d.ts.map +1 -0
- package/dist/live.d.ts +2 -0
- package/dist/live.js +126 -0
- package/dist/live.js.map +1 -0
- package/dist/matching-DF5F9Us8.d.ts +100 -0
- package/dist/matching-DF5F9Us8.d.ts.map +1 -0
- package/dist/matching.d.ts +2 -0
- package/dist/matching.js +172 -0
- package/dist/matching.js.map +1 -0
- package/dist/model-B6zk1NMB.d.ts +710 -0
- package/dist/model-B6zk1NMB.d.ts.map +1 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +1308 -0
- package/dist/model.js.map +1 -0
- package/dist/mutation.d.ts +2 -0
- package/dist/mutation.js +612 -0
- package/dist/mutation.js.map +1 -0
- package/dist/optimistic-CECsfNGY.d.ts +523 -0
- package/dist/optimistic-CECsfNGY.d.ts.map +1 -0
- package/dist/optimistic.d.ts +2 -0
- package/dist/optimistic.js +336 -0
- package/dist/optimistic.js.map +1 -0
- package/dist/outbox-DNuc4kZp.d.ts +259 -0
- package/dist/outbox-DNuc4kZp.d.ts.map +1 -0
- package/dist/outbox.d.ts +2 -0
- package/dist/outbox.js +326 -0
- package/dist/outbox.js.map +1 -0
- package/dist/patchValues.d.ts +10 -0
- package/dist/patchValues.d.ts.map +1 -0
- package/dist/patchValues.js +57 -0
- package/dist/patchValues.js.map +1 -0
- package/dist/persistence-CCMnvg7-.d.ts +114 -0
- package/dist/persistence-CCMnvg7-.d.ts.map +1 -0
- package/dist/persistence.d.ts +2 -0
- package/dist/persistence.js +205 -0
- package/dist/persistence.js.map +1 -0
- package/dist/placement-jZdsIpmj.d.ts +50 -0
- package/dist/placement-jZdsIpmj.d.ts.map +1 -0
- package/dist/placement.d.ts +2 -0
- package/dist/placement.js +116 -0
- package/dist/placement.js.map +1 -0
- package/dist/plan-D9cftxC_.d.ts +35 -0
- package/dist/plan-D9cftxC_.d.ts.map +1 -0
- package/dist/plan.d.ts +2 -0
- package/dist/plan.js +100 -0
- package/dist/plan.js.map +1 -0
- package/dist/policy-DsWHmIAq.d.ts +42 -0
- package/dist/policy-DsWHmIAq.d.ts.map +1 -0
- package/dist/policy.d.ts +2 -0
- package/dist/policy.js +32 -0
- package/dist/policy.js.map +1 -0
- package/dist/{port.d.mts → port.d.ts} +3 -2
- package/dist/port.d.ts.map +1 -0
- package/dist/{port.mjs → port.js} +3 -2
- package/dist/port.js.map +1 -0
- package/dist/query-BuIU93xA.d.ts +141 -0
- package/dist/query-BuIU93xA.d.ts.map +1 -0
- package/dist/query.d.ts +2 -0
- package/dist/query.js +0 -0
- package/dist/query.js.map +1 -0
- package/dist/record.d.ts +11 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +21 -0
- package/dist/record.js.map +1 -0
- package/dist/relation-BLBGOZom.d.ts +59 -0
- package/dist/relation-BLBGOZom.d.ts.map +1 -0
- package/dist/relation.d.ts +2 -0
- package/dist/relation.js +93 -0
- package/dist/relation.js.map +1 -0
- package/dist/remoteData-mK1suYi1.d.ts +148 -0
- package/dist/remoteData-mK1suYi1.d.ts.map +1 -0
- package/dist/remoteData.d.ts +2 -0
- package/dist/remoteData.js +179 -0
- package/dist/remoteData.js.map +1 -0
- package/dist/reporting.d.ts +12 -0
- package/dist/reporting.d.ts.map +1 -0
- package/dist/reporting.js +45 -0
- package/dist/reporting.js.map +1 -0
- package/dist/requirement-D57S2LpH.d.ts +64 -0
- package/dist/requirement-D57S2LpH.d.ts.map +1 -0
- package/dist/requirement.d.ts +2 -0
- package/dist/requirement.js +114 -0
- package/dist/requirement.js.map +1 -0
- package/dist/resume-Ct5R6yli.d.ts +92 -0
- package/dist/resume-Ct5R6yli.d.ts.map +1 -0
- package/dist/resume.d.ts +2 -0
- package/dist/resume.js +222 -0
- package/dist/resume.js.map +1 -0
- package/dist/retain-BfNTvBRb.d.ts +48 -0
- package/dist/retain-BfNTvBRb.d.ts.map +1 -0
- package/dist/retain.d.ts +2 -0
- package/dist/retain.js +107 -0
- package/dist/retain.js.map +1 -0
- package/dist/selection.d.ts +2 -0
- package/dist/selection.js +1 -0
- package/dist/shown.d.ts +20 -0
- package/dist/shown.d.ts.map +1 -0
- package/dist/shown.js +40 -0
- package/dist/shown.js.map +1 -0
- package/dist/sse-C7LhdbiQ.d.ts +17 -0
- package/dist/sse-C7LhdbiQ.d.ts.map +1 -0
- package/dist/sse.d.ts +2 -0
- package/dist/sse.js +127 -0
- package/dist/sse.js.map +1 -0
- package/dist/store-ah2qjcJW.d.ts +100 -0
- package/dist/store-ah2qjcJW.d.ts.map +1 -0
- package/dist/store.d.ts +2 -0
- package/dist/store.js +218 -0
- package/dist/store.js.map +1 -0
- package/dist/testing.d.ts +133 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +0 -0
- package/dist/testing.js.map +1 -0
- package/dist/why-kgp_Wr3z.d.ts +483 -0
- package/dist/why-kgp_Wr3z.d.ts.map +1 -0
- package/dist/why.d.ts +2 -0
- package/dist/why.js +293 -0
- package/dist/why.js.map +1 -0
- package/dist/whyReasons.d.ts +42 -0
- package/dist/whyReasons.d.ts.map +1 -0
- package/dist/whyReasons.js +211 -0
- package/dist/whyReasons.js.map +1 -0
- package/dist/wire-DthPcTM6.d.ts +787 -0
- package/dist/wire-DthPcTM6.d.ts.map +1 -0
- package/dist/wire.d.ts +2 -0
- package/dist/wire.js +329 -0
- package/dist/wire.js.map +1 -0
- package/package.json +19 -14
- package/dist/index-BEvVAL76.d.mts +0 -4097
- package/dist/index-BEvVAL76.d.mts.map +0 -1
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +0 -1
- package/dist/port.d.mts.map +0 -1
- package/dist/port.mjs.map +0 -1
- package/dist/src-BDqn7Ocr.mjs +0 -6344
- package/dist/src-BDqn7Ocr.mjs.map +0 -1
package/dist/mutation.js
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import { own, put } from "./record.js";
|
|
2
|
+
import { refParts } from "./relation.js";
|
|
3
|
+
import { emptyStore, entityKey, tombstone, writeEntities } from "./store.js";
|
|
4
|
+
import { holderOf } from "./held.js";
|
|
5
|
+
import { Data, Effect, Match, Option, Record, Schema } from "effect";
|
|
6
|
+
import { Derivation, Entity, Held, Write } from "foldkit-entity";
|
|
7
|
+
// src/mutation.ts
|
|
8
|
+
/**
|
|
9
|
+
* Mutation status and idempotent reconciliation.
|
|
10
|
+
*
|
|
11
|
+
* A mutation result is reconciled into the store **at most once per
|
|
12
|
+
* `requestId`**, so a transport retry cannot apply the same change twice. An
|
|
13
|
+
* unknown or already-applied result is a no-op.
|
|
14
|
+
*/
|
|
15
|
+
const emptyMutationState = {
|
|
16
|
+
pending: /* @__PURE__ */ new Set(),
|
|
17
|
+
applied: /* @__PURE__ */ new Set(),
|
|
18
|
+
failed: /* @__PURE__ */ new Set(),
|
|
19
|
+
errors: /* @__PURE__ */ new Map(),
|
|
20
|
+
operations: /* @__PURE__ */ new Map(),
|
|
21
|
+
writes: /* @__PURE__ */ new Map(),
|
|
22
|
+
sequence: 0,
|
|
23
|
+
order: /* @__PURE__ */ new Map(),
|
|
24
|
+
settled: /* @__PURE__ */ new Map()
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* How many settled request ids a mutation state retains. A retry settles well
|
|
28
|
+
* inside this window, so bounding the ledger stops a long-lived application
|
|
29
|
+
* from growing it without limit; a retry older than the window would re-apply
|
|
30
|
+
* its entities.
|
|
31
|
+
*/
|
|
32
|
+
const MUTATION_ID_WINDOW = 1024;
|
|
33
|
+
/** Records `id` as most recent, evicting the oldest once the window is full. */
|
|
34
|
+
const remember = (ids, id) => {
|
|
35
|
+
const next = new Set(ids);
|
|
36
|
+
next.delete(id);
|
|
37
|
+
next.add(id);
|
|
38
|
+
if (next.size <= MUTATION_ID_WINDOW) return next;
|
|
39
|
+
return new Set([...next].slice(next.size - MUTATION_ID_WINDOW));
|
|
40
|
+
};
|
|
41
|
+
const beginMutation = (state, requestId, write) => ({
|
|
42
|
+
...state,
|
|
43
|
+
pending: /* @__PURE__ */ new Set([...state.pending, requestId]),
|
|
44
|
+
writes: write === void 0 ? state.writes : new Map([...state.writes, [requestId, write]]),
|
|
45
|
+
sequence: state.sequence + 1,
|
|
46
|
+
order: new Map([...state.order, [requestId, state.sequence + 1]])
|
|
47
|
+
});
|
|
48
|
+
const failMutation = (state, requestId, error, operation) => {
|
|
49
|
+
if (!state.pending.has(requestId)) return state;
|
|
50
|
+
const pending = new Set(state.pending);
|
|
51
|
+
pending.delete(requestId);
|
|
52
|
+
const failed = remember(state.failed, requestId);
|
|
53
|
+
const errors = new Map([...state.errors, [requestId, error]]);
|
|
54
|
+
const operations = new Map([...state.operations, [requestId, operation]]);
|
|
55
|
+
const writes = new Map(state.writes);
|
|
56
|
+
for (const id of errors.keys()) if (!failed.has(id)) errors.delete(id);
|
|
57
|
+
for (const id of operations.keys()) if (!failed.has(id)) operations.delete(id);
|
|
58
|
+
for (const id of writes.keys()) if (!failed.has(id) && !pending.has(id)) writes.delete(id);
|
|
59
|
+
return {
|
|
60
|
+
...state,
|
|
61
|
+
pending,
|
|
62
|
+
failed,
|
|
63
|
+
errors,
|
|
64
|
+
operations,
|
|
65
|
+
writes,
|
|
66
|
+
...ordered(state, requestId, [])
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* `order` without a request that settled, and `settled` with what its answer
|
|
71
|
+
* wrote (`marks`), each kept only while a request started before it is pending.
|
|
72
|
+
*/
|
|
73
|
+
const ordered = (state, requestId, marks) => {
|
|
74
|
+
const at = state.order.get(requestId);
|
|
75
|
+
const order = new Map(state.order);
|
|
76
|
+
order.delete(requestId);
|
|
77
|
+
const earliest = Math.min(...order.values());
|
|
78
|
+
const settled = /* @__PURE__ */ new Map();
|
|
79
|
+
for (const [mark, by] of state.settled) if (by > earliest) settled.set(mark, by);
|
|
80
|
+
if (at !== void 0 && at > earliest) for (const mark of marks) settled.set(mark, at);
|
|
81
|
+
return {
|
|
82
|
+
order,
|
|
83
|
+
settled
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
/** The ledger with a request's failure forgotten: its id, error, operation and write. */
|
|
87
|
+
const forgetFailure = (state, requestId) => {
|
|
88
|
+
const without = (map) => {
|
|
89
|
+
if (!map.has(requestId)) return map;
|
|
90
|
+
const next = new Map(map);
|
|
91
|
+
next.delete(requestId);
|
|
92
|
+
return next;
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
...state,
|
|
96
|
+
failed: state.failed.has(requestId) ? new Set([...state.failed].filter((id) => id !== requestId)) : state.failed,
|
|
97
|
+
errors: without(state.errors),
|
|
98
|
+
operations: without(state.operations),
|
|
99
|
+
writes: without(state.writes)
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
const mutationStatus = (state, requestId) => {
|
|
103
|
+
if (state.pending.has(requestId)) return { _tag: "Pending" };
|
|
104
|
+
if (state.applied.has(requestId)) return { _tag: "Applied" };
|
|
105
|
+
const error = state.errors.get(requestId);
|
|
106
|
+
return error === void 0 ? { _tag: "Unknown" } : {
|
|
107
|
+
_tag: "Failed",
|
|
108
|
+
error
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* What requests started after `requestId` already settled, which its answer
|
|
113
|
+
* leaves alone. The server applied requests in the order they started, so a
|
|
114
|
+
* later one's answer is newer: `newer` holds of a field it wrote (its mark)
|
|
115
|
+
* and of a row it deleted (its key), `rewritten` of a row it wrote any field
|
|
116
|
+
* of. An answer of a request no longer pending (after `forget`, or a failure)
|
|
117
|
+
* has no order, and nothing is newer than it.
|
|
118
|
+
*/
|
|
119
|
+
const settledLater = (state, requestId) => {
|
|
120
|
+
const at = state.order.get(requestId) ?? Infinity;
|
|
121
|
+
return {
|
|
122
|
+
newer: (mark) => (state.settled.get(mark) ?? -Infinity) > at,
|
|
123
|
+
rewritten: (key) => [...state.settled].some(([mark, by]) => by > at && mark.startsWith(`${key}\u0000`))
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Applies a mutation result once and marks the request settled. A second call
|
|
128
|
+
* with the same `requestId` (a retry, or a live event describing the same change)
|
|
129
|
+
* does not re-apply the entities, but still clears `pending`.
|
|
130
|
+
*/
|
|
131
|
+
const reconcileMutation = (store, state, requestId, answer) => {
|
|
132
|
+
const repeated = state.applied.has(requestId);
|
|
133
|
+
if (repeated && !state.pending.has(requestId) && !state.failed.has(requestId)) return {
|
|
134
|
+
store,
|
|
135
|
+
state
|
|
136
|
+
};
|
|
137
|
+
const { newer, rewritten } = settledLater(state, requestId);
|
|
138
|
+
const writes = repeated ? [] : answer.entities.flatMap((entity) => {
|
|
139
|
+
const key = entityKey(entity.entity, entity.id);
|
|
140
|
+
if (newer(key)) return [];
|
|
141
|
+
const values = Object.fromEntries(Object.entries(entity.values).filter(([field]) => !newer(`${key}\u0000${field}`)));
|
|
142
|
+
return Object.keys(values).length === 0 ? [] : [{
|
|
143
|
+
key,
|
|
144
|
+
values
|
|
145
|
+
}];
|
|
146
|
+
});
|
|
147
|
+
const deleted = repeated ? [] : (answer.deleted ?? []).map((gone) => entityKey(gone.entity, gone.id)).filter((key) => !rewritten(key));
|
|
148
|
+
const next = deleted.reduce(tombstone, writeEntities(store, writes, answer.now));
|
|
149
|
+
const applied = remember(state.applied, requestId);
|
|
150
|
+
const pending = new Set(state.pending);
|
|
151
|
+
pending.delete(requestId);
|
|
152
|
+
const marks = [...writes.flatMap((write) => Object.keys(write.values).map((field) => `${write.key}\u0000${field}`)), ...deleted];
|
|
153
|
+
return {
|
|
154
|
+
store: next,
|
|
155
|
+
state: {
|
|
156
|
+
...forgetFailure(state, requestId),
|
|
157
|
+
applied,
|
|
158
|
+
pending,
|
|
159
|
+
...ordered(state, requestId, marks)
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
const conflict = Schema.TaggedStruct("Conflict", {});
|
|
164
|
+
const unmet = Schema.TaggedStruct("Unmet", {});
|
|
165
|
+
/**
|
|
166
|
+
* Another row already holds the id an insert names: its Entity and that id,
|
|
167
|
+
* both the caller's own, and nothing of the row, which may be hidden from them.
|
|
168
|
+
*/
|
|
169
|
+
const taken = Schema.TaggedStruct("Taken", {
|
|
170
|
+
entity: Schema.String,
|
|
171
|
+
id: Schema.String
|
|
172
|
+
});
|
|
173
|
+
/**
|
|
174
|
+
* The row the write would leave breaks one of its Entity's invariants: which
|
|
175
|
+
* one (its Entity and its id, since an id is unique only within one), what to
|
|
176
|
+
* tell the author, and the fields to show it beside. None and empty when the
|
|
177
|
+
* invariant reads a member hidden from whoever asked.
|
|
178
|
+
*/
|
|
179
|
+
const invalid = Schema.TaggedStruct("Invalid", {
|
|
180
|
+
entity: Schema.OptionFromOptionalNullOr(Schema.String),
|
|
181
|
+
rule: Schema.OptionFromNullOr(Schema.String),
|
|
182
|
+
message: Schema.String,
|
|
183
|
+
path: Schema.Array(Schema.String)
|
|
184
|
+
});
|
|
185
|
+
const conflictOrInvalid = Schema.Union([conflict, invalid]);
|
|
186
|
+
const conflictUnmetOrInvalid = Schema.Union([
|
|
187
|
+
conflict,
|
|
188
|
+
unmet,
|
|
189
|
+
invalid
|
|
190
|
+
]);
|
|
191
|
+
const takenOrInvalid = Schema.Union([taken, invalid]);
|
|
192
|
+
/** What the constructors typed a write by: its kind, and whether it holds a `when`. */
|
|
193
|
+
const refusalOfWrite = (write) => {
|
|
194
|
+
const moved = () => Option.isSome(write.when) ? conflictUnmetOrInvalid : conflictOrInvalid;
|
|
195
|
+
return Match.value(write).pipe(Match.tagsExhaustive({
|
|
196
|
+
Insert: () => takenOrInvalid,
|
|
197
|
+
Update: moved,
|
|
198
|
+
Delete: moved
|
|
199
|
+
}));
|
|
200
|
+
};
|
|
201
|
+
const anyField = Schema.TaggedStruct("Field", {
|
|
202
|
+
key: Schema.String,
|
|
203
|
+
reason: Schema.Unknown
|
|
204
|
+
});
|
|
205
|
+
/** Refusals a form or an editor knows how to show. */
|
|
206
|
+
const Refusal = {
|
|
207
|
+
/** One input key was refused, for `reason`, which a form shows beside that key. */
|
|
208
|
+
field: (key, reason) => Schema.TaggedStruct("Field", {
|
|
209
|
+
key: Schema.Literal(key),
|
|
210
|
+
reason
|
|
211
|
+
}),
|
|
212
|
+
/** The row moved on since the client read it; the author decides what wins. */
|
|
213
|
+
conflict,
|
|
214
|
+
/** The row does not hold what the write's `when` asks of it, where it would commit. */
|
|
215
|
+
unmet,
|
|
216
|
+
/** The id an insert names is taken: another create made it, or a retry the server could not tell. */
|
|
217
|
+
taken,
|
|
218
|
+
/** The row the write would leave breaks an invariant of its Entity. */
|
|
219
|
+
invalid,
|
|
220
|
+
/** Whether a refusal, of whatever mutation, is a `Refusal.field`. */
|
|
221
|
+
isField: Schema.is(anyField),
|
|
222
|
+
/** Whether a refusal, of whatever mutation, is `Refusal.conflict`. */
|
|
223
|
+
isConflict: Schema.is(conflict),
|
|
224
|
+
/** Whether a refusal, of whatever mutation, is `Refusal.unmet`. */
|
|
225
|
+
isUnmet: Schema.is(unmet),
|
|
226
|
+
/** Whether a refusal, of whatever mutation, is `Refusal.taken`. */
|
|
227
|
+
isTaken: Schema.is(taken),
|
|
228
|
+
/** Whether a refusal, of whatever mutation, is `Refusal.invalid`. */
|
|
229
|
+
isInvalid: Schema.is(invalid),
|
|
230
|
+
/**
|
|
231
|
+
* Whether a refusal is `Refusal.invalid` naming this invariant, by its
|
|
232
|
+
* Entity and its id: the declaration `Entity.invariant` returned, once
|
|
233
|
+
* attached, or one `Entity.invariants` reads. An invariant over a member
|
|
234
|
+
* hidden from the asker names none, so it matches nothing.
|
|
235
|
+
*/
|
|
236
|
+
isInvalidBy: (refusal, invariant) => Schema.is(invalid)(refusal) && Option.contains(refusal.entity, invariant.entity) && Option.contains(refusal.rule, invariant.id)
|
|
237
|
+
};
|
|
238
|
+
/** The codec itself, or a `Schema.Struct` over the fields. */
|
|
239
|
+
const schemaOf = (shape) => Schema.isSchema(shape) ? shape : Schema.Struct(shape);
|
|
240
|
+
/**
|
|
241
|
+
* A bound write as the store patches it: its fields as they are, and each
|
|
242
|
+
* relation it points as the ref key the store holds (`'User:u1'`), or null.
|
|
243
|
+
*/
|
|
244
|
+
const patchOfWrite = (bound) => ({
|
|
245
|
+
entity: bound.entity,
|
|
246
|
+
id: bound.id,
|
|
247
|
+
values: {
|
|
248
|
+
...bound.values,
|
|
249
|
+
...Record.map(bound.links, (link) => Option.match(link, {
|
|
250
|
+
onNone: () => null,
|
|
251
|
+
onSome: (ref) => entityKey(ref.entity, ref.id)
|
|
252
|
+
}))
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
/**
|
|
256
|
+
* One member of a row as `store` holds it, for a path from the viewer: a row
|
|
257
|
+
* known gone reaches no row (null), and a row or a field never read is unheld.
|
|
258
|
+
*/
|
|
259
|
+
const heldIn = (store) => (row, key) => {
|
|
260
|
+
const entry = store[entityKey(row.entity, row.id)];
|
|
261
|
+
if (entry === void 0) return Effect.succeed(Held.unheld(`${row.entity} ${row.id} is not held`));
|
|
262
|
+
if (entry.tombstone) return Effect.succeed(Held.of(null));
|
|
263
|
+
return Effect.succeed(entry.present.has(key) ? Held.of(entry.values[key]) : Held.unheld(`${row.entity}.${key} of ${row.id} is not held`));
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* A write bound to its input, with what its Entity sets from the context
|
|
267
|
+
* (`Entity.sets`) as `context` gives it: who asks (`Remote.make`'s viewer),
|
|
268
|
+
* what their row holds (read from `store`), and when the reader acted
|
|
269
|
+
* (`Data.mutate`'s `now`). A member with no value to give is left out of the
|
|
270
|
+
* prediction, and the answer brings it.
|
|
271
|
+
*/
|
|
272
|
+
const contextBound = (write, input, keys, context, store) => {
|
|
273
|
+
const bound = Write.bind(write, input, keys);
|
|
274
|
+
const set = Effect.runSync(Write.contextValues(write, context, heldIn(store)));
|
|
275
|
+
return {
|
|
276
|
+
...bound,
|
|
277
|
+
values: {
|
|
278
|
+
...bound.values,
|
|
279
|
+
...set.values
|
|
280
|
+
},
|
|
281
|
+
links: {
|
|
282
|
+
...bound.links,
|
|
283
|
+
...set.links
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
};
|
|
287
|
+
const nobody = {
|
|
288
|
+
viewer: Option.none(),
|
|
289
|
+
now: Option.none()
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* What a declared write shows before the server answers: an update's or an
|
|
293
|
+
* insert's row as a patch (an update's only for the keys named, when some
|
|
294
|
+
* are), and an update's increments and computed fields, worked out over the
|
|
295
|
+
* row the store holds when it is read. A delete shows its row deleted. What
|
|
296
|
+
* the context sets is filled from `context` and `store`, where it can be.
|
|
297
|
+
*/
|
|
298
|
+
const optimisticOfWrite = (write, input, keys, context = nobody, store = emptyStore) => Match.value(write).pipe(Match.tagsExhaustive({
|
|
299
|
+
Update: () => {
|
|
300
|
+
const bound = contextBound(write, input, keys, context, store);
|
|
301
|
+
return [{
|
|
302
|
+
...patchOfWrite(bound),
|
|
303
|
+
...Object.keys(bound.increments).length === 0 ? {} : { adds: bound.increments },
|
|
304
|
+
...Object.keys(bound.assignments).length === 0 ? {} : { computes: bound.assignments }
|
|
305
|
+
}];
|
|
306
|
+
},
|
|
307
|
+
Insert: () => {
|
|
308
|
+
const patch = patchOfWrite(contextBound(write, input, void 0, context, store));
|
|
309
|
+
return [{
|
|
310
|
+
...patch,
|
|
311
|
+
values: {
|
|
312
|
+
id: patch.id,
|
|
313
|
+
...patch.values
|
|
314
|
+
}
|
|
315
|
+
}];
|
|
316
|
+
},
|
|
317
|
+
Delete: () => {
|
|
318
|
+
const { entity, id } = Write.bind(write, input);
|
|
319
|
+
return [{
|
|
320
|
+
_tag: "Delete",
|
|
321
|
+
entity,
|
|
322
|
+
id
|
|
323
|
+
}];
|
|
324
|
+
}
|
|
325
|
+
}));
|
|
326
|
+
const DerivedBy = Data.taggedEnum();
|
|
327
|
+
/**
|
|
328
|
+
* The derived members a declared write moves, as pending patches of the rows
|
|
329
|
+
* its row points at. A count: an insert adds one where it points, a delete
|
|
330
|
+
* takes one away where the row pointed, and a relink does both. An exists:
|
|
331
|
+
* a row now pointing here and at `viewer` is found (its id), and a row the
|
|
332
|
+
* store shows as found by this one is no longer, once the write deletes it or
|
|
333
|
+
* points it elsewhere.
|
|
334
|
+
*
|
|
335
|
+
* Where the row pointed before is read from `store`: the row as held, or,
|
|
336
|
+
* when the store holds an exists found by this row, the row it is found on
|
|
337
|
+
* and the viewer. When neither says, nothing is predicted and the answer
|
|
338
|
+
* decides. A member is moved only on a row that `store` holds it on.
|
|
339
|
+
*/
|
|
340
|
+
const derivedOfWrite = (write, input, keys, derived, store, context) => {
|
|
341
|
+
if (derived.length === 0) return [];
|
|
342
|
+
const bound = contextBound(write, input, write._tag === "Update" ? keys : void 0, context, store);
|
|
343
|
+
const row = store[entityKey(bound.entity, bound.id)];
|
|
344
|
+
const held = row === void 0 || row.tombstone ? void 0 : row;
|
|
345
|
+
const viewerKey = Option.map(context.viewer, (ref) => entityKey(ref.entity, ref.id));
|
|
346
|
+
/** What a live row holds in `field`, as the store has it; none for a row gone or not held. */
|
|
347
|
+
const holding = (key, field) => Option.flatMap(Option.fromUndefinedOr(store[key]), (entry) => entry.tombstone ? Option.none() : Option.some(entry.values[field]));
|
|
348
|
+
const found = /* @__PURE__ */ new Map();
|
|
349
|
+
if (write._tag !== "Insert") for (const member of derived.filter(DerivedBy.$is("Exists"))) found.set(member, Object.keys(store).filter((key) => key.startsWith(`${member.owner}:`) && Option.contains(holding(key, member.field), bound.id)));
|
|
350
|
+
const before = (relation) => {
|
|
351
|
+
if (held !== void 0 && held.present.has(relation)) return Option.some(held.values[relation]);
|
|
352
|
+
for (const [member, rows] of found) {
|
|
353
|
+
if (rows.length === 0) continue;
|
|
354
|
+
if (member.relation === relation) return Option.some(rows[0]);
|
|
355
|
+
if (member.viewer === relation && Option.isSome(viewerKey)) return viewerKey;
|
|
356
|
+
}
|
|
357
|
+
return write._tag === "Insert" ? Option.some(null) : Option.none();
|
|
358
|
+
};
|
|
359
|
+
const patched = patchOfWrite(bound).values;
|
|
360
|
+
const after = (relation) => Match.value(write).pipe(Match.tagsExhaustive({
|
|
361
|
+
Delete: () => Option.some(null),
|
|
362
|
+
Insert: () => Option.some(Object.hasOwn(bound.links, relation) ? patched[relation] : null),
|
|
363
|
+
Update: () => Object.hasOwn(bound.links, relation) ? Option.some(patched[relation]) : before(relation)
|
|
364
|
+
}));
|
|
365
|
+
const moves = /* @__PURE__ */ new Map();
|
|
366
|
+
const at = (ref, owner) => {
|
|
367
|
+
const move = moves.get(ref) ?? {
|
|
368
|
+
owner,
|
|
369
|
+
values: {},
|
|
370
|
+
adds: {}
|
|
371
|
+
};
|
|
372
|
+
moves.set(ref, move);
|
|
373
|
+
return move;
|
|
374
|
+
};
|
|
375
|
+
const count = (ref, owner, field, by) => {
|
|
376
|
+
if (typeof ref !== "string") return;
|
|
377
|
+
if (!Option.exists(holding(ref, field), (value) => typeof value === "number")) return;
|
|
378
|
+
const move = at(ref, owner);
|
|
379
|
+
put(move.adds, field, (own(move.adds, field) ?? 0) + by);
|
|
380
|
+
};
|
|
381
|
+
for (const member of derived) DerivedBy.$match(member, {
|
|
382
|
+
Count: ({ relation, owner, field }) => {
|
|
383
|
+
const from = before(relation);
|
|
384
|
+
const to = after(relation);
|
|
385
|
+
if (Option.isNone(from) || Option.isNone(to) || from.value === to.value) return;
|
|
386
|
+
count(from.value, owner, field, -1);
|
|
387
|
+
count(to.value, owner, field, 1);
|
|
388
|
+
},
|
|
389
|
+
Exists: (exists) => {
|
|
390
|
+
const { relation, viewer: by, owner, field } = exists;
|
|
391
|
+
const pointed = after(relation);
|
|
392
|
+
const viewing = Option.flatMap(after(by), (ref) => Option.map(viewerKey, (key) => ref === key));
|
|
393
|
+
for (const ref of found.get(exists) ?? []) if (Option.exists(pointed, (to) => to !== ref) || Option.contains(viewing, false)) put(at(ref, owner).values, field, null);
|
|
394
|
+
if (!Option.contains(viewing, true) || Option.isNone(pointed)) return;
|
|
395
|
+
const to = pointed.value;
|
|
396
|
+
if (typeof to === "string" && Option.contains(holding(to, field), null)) put(at(to, owner).values, field, bound.id);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
return [...moves].flatMap(([ref, { owner, values, adds }]) => {
|
|
400
|
+
const moved = Object.fromEntries(Object.entries(adds).filter(([, by]) => by !== 0));
|
|
401
|
+
return Object.keys(values).length === 0 && Object.keys(moved).length === 0 ? [] : [{
|
|
402
|
+
entity: owner,
|
|
403
|
+
id: refParts(ref).id,
|
|
404
|
+
values,
|
|
405
|
+
...Object.keys(moved).length === 0 ? {} : { adds: moved }
|
|
406
|
+
}];
|
|
407
|
+
});
|
|
408
|
+
};
|
|
409
|
+
/**
|
|
410
|
+
* A new row's derived members over relations: each count 0 and each exists
|
|
411
|
+
* none, since no row points at a row not yet made. A later pending write that
|
|
412
|
+
* points at it moves them as it would any held row's.
|
|
413
|
+
*/
|
|
414
|
+
const freshDerived = (write, input, derived) => {
|
|
415
|
+
const entity = write.input.entity.name;
|
|
416
|
+
const values = {};
|
|
417
|
+
for (const members of derived.values()) for (const member of members) if (member.owner === entity) put(values, member.field, DerivedBy.$match(member, {
|
|
418
|
+
Count: () => 0,
|
|
419
|
+
Exists: () => null
|
|
420
|
+
}));
|
|
421
|
+
if (Object.keys(values).length === 0) return [];
|
|
422
|
+
const { id } = Write.bind(write, input);
|
|
423
|
+
return [{
|
|
424
|
+
entity,
|
|
425
|
+
id,
|
|
426
|
+
values
|
|
427
|
+
}];
|
|
428
|
+
};
|
|
429
|
+
/** Each supplied member of `entity` that says how a client foresees it, by key. */
|
|
430
|
+
const predicted = /* @__PURE__ */ new WeakMap();
|
|
431
|
+
const predictorsOf = (entity) => {
|
|
432
|
+
const known = predicted.get(entity);
|
|
433
|
+
if (known !== void 0) return known;
|
|
434
|
+
const derived = entity.derived;
|
|
435
|
+
const found = Object.entries(derived).flatMap(([key, member]) => Derivation.$match(member.derives, {
|
|
436
|
+
Supplied: ({ predict }) => Option.match(predict, {
|
|
437
|
+
onNone: () => [],
|
|
438
|
+
onSome: (run) => [[key, run]]
|
|
439
|
+
}),
|
|
440
|
+
Count: () => [],
|
|
441
|
+
Exists: () => []
|
|
442
|
+
}));
|
|
443
|
+
predicted.set(entity, found);
|
|
444
|
+
return found;
|
|
445
|
+
};
|
|
446
|
+
/**
|
|
447
|
+
* The supplied members a write's row foresees (`Derived.make`'s `predict`),
|
|
448
|
+
* judged over the row it leaves, held over the store with the write's values:
|
|
449
|
+
* a member foreseen as none is left out, so the row waits for the answer.
|
|
450
|
+
*/
|
|
451
|
+
const suppliedOfWrite = (write, own, by, store) => {
|
|
452
|
+
const entity = write.input.entity;
|
|
453
|
+
const predictors = predictorsOf(entity);
|
|
454
|
+
if (predictors.length === 0) return [];
|
|
455
|
+
const holder = holderOf(store);
|
|
456
|
+
const row = holder.written(entity, own.id, own.values, [...Object.keys(own.adds ?? {}), ...Object.keys(own.computes ?? {})]);
|
|
457
|
+
const values = {};
|
|
458
|
+
for (const [key, predict] of predictors) Option.map(predict({
|
|
459
|
+
entity,
|
|
460
|
+
row,
|
|
461
|
+
rows: holder.rows,
|
|
462
|
+
context: by
|
|
463
|
+
}), (value) => {
|
|
464
|
+
put(values, key, value);
|
|
465
|
+
});
|
|
466
|
+
return Object.keys(values).length === 0 ? [] : [{
|
|
467
|
+
entity: entity.name,
|
|
468
|
+
id: own.id,
|
|
469
|
+
values
|
|
470
|
+
}];
|
|
471
|
+
};
|
|
472
|
+
/**
|
|
473
|
+
* Everything a declared write shows before the server answers, from its
|
|
474
|
+
* declarations alone: its own row (`optimisticOfWrite`), with what the
|
|
475
|
+
* context sets filled from `viewer` and `now`, a new row's
|
|
476
|
+
* derived members (`freshDerived`), what its supplied members foresee
|
|
477
|
+
* (`suppliedOfWrite`, Guard's `may`), and the derived members it moves on
|
|
478
|
+
* the rows it points at (`derivedOfWrite`). `derived` is the registry's, by
|
|
479
|
+
* the pointing Entity; `store` is the store as shown, layers and all.
|
|
480
|
+
*/
|
|
481
|
+
const predictionOfWrite = (write, input, keys, context) => {
|
|
482
|
+
const by = {
|
|
483
|
+
viewer: context.viewer,
|
|
484
|
+
now: context.now
|
|
485
|
+
};
|
|
486
|
+
const own = optimisticOfWrite(write, input, keys, by, context.store);
|
|
487
|
+
return [
|
|
488
|
+
...Match.value(write).pipe(Match.tagsExhaustive({
|
|
489
|
+
Insert: () => freshDerived(write, input, context.derived),
|
|
490
|
+
Update: () => [],
|
|
491
|
+
Delete: () => []
|
|
492
|
+
})),
|
|
493
|
+
...own,
|
|
494
|
+
...own.flatMap((patch) => "_tag" in patch ? [] : suppliedOfWrite(write, patch, by, context.store)),
|
|
495
|
+
...derivedOfWrite(write, input, keys, context.derived.get(write.input.entity.name) ?? [], context.store, by)
|
|
496
|
+
];
|
|
497
|
+
};
|
|
498
|
+
/** The held fields of `row`, plain and encoded, as an invariant is judged over them. */
|
|
499
|
+
const plainRow = (entity, row) => {
|
|
500
|
+
const plain = {};
|
|
501
|
+
for (const key of Object.keys(entity.fields)) Held.match(row.field(key), {
|
|
502
|
+
onHeld: (value) => {
|
|
503
|
+
put(plain, key, value);
|
|
504
|
+
},
|
|
505
|
+
onUnheld: () => void 0
|
|
506
|
+
});
|
|
507
|
+
return plain;
|
|
508
|
+
};
|
|
509
|
+
/**
|
|
510
|
+
* The invariant a declared write's row would break, as the client holds it:
|
|
511
|
+
* the store's row with the write's values over it, a field the write moves
|
|
512
|
+
* (an increment, a computed value) not held, and for an update the row
|
|
513
|
+
* before. Only a definite failure counts: an invariant over a field the
|
|
514
|
+
* client lacks is undecided, and refuses nothing. A delete leaves no row.
|
|
515
|
+
*/
|
|
516
|
+
const brokenByWrite = (write, input, keys, context, store, operators) => {
|
|
517
|
+
const entity = write.input.entity;
|
|
518
|
+
const patch = optimisticOfWrite(write, input, keys, context, store).find((each) => !("_tag" in each));
|
|
519
|
+
if (patch === void 0) return Option.none();
|
|
520
|
+
const holder = holderOf(store);
|
|
521
|
+
const after = plainRow(entity, holder.written(entity, patch.id, patch.values, [...Object.keys(patch.adds ?? {}), ...Object.keys(patch.computes ?? {})]));
|
|
522
|
+
const before = Option.getOrUndefined(Option.map(holder.rows.row(entity, patch.id), (row) => plainRow(entity, row)));
|
|
523
|
+
const judged = Entity.judge(entity, write._tag === "Insert" || before === void 0 ? { after } : {
|
|
524
|
+
after,
|
|
525
|
+
before
|
|
526
|
+
}, operators);
|
|
527
|
+
return judged._tag === "Invalid" ? Option.fromUndefinedOr(judged.failures[0]) : Option.none();
|
|
528
|
+
};
|
|
529
|
+
/** Each supplied member of `entity` that says which write its value refuses, by key. */
|
|
530
|
+
const forbidding = /* @__PURE__ */ new WeakMap();
|
|
531
|
+
const forbiddersOf = (entity) => {
|
|
532
|
+
const known = forbidding.get(entity);
|
|
533
|
+
if (known !== void 0) return known;
|
|
534
|
+
const derived = entity.derived;
|
|
535
|
+
const found = Object.entries(derived).flatMap(([key, member]) => Derivation.$match(member.derives, {
|
|
536
|
+
Supplied: ({ forbids }) => Option.match(forbids, {
|
|
537
|
+
onNone: () => [],
|
|
538
|
+
onSome: (run) => [[key, run]]
|
|
539
|
+
}),
|
|
540
|
+
Count: () => [],
|
|
541
|
+
Exists: () => []
|
|
542
|
+
}));
|
|
543
|
+
forbidding.set(entity, found);
|
|
544
|
+
return found;
|
|
545
|
+
};
|
|
546
|
+
/**
|
|
547
|
+
* Why a declared write is refused by what the client holds of its row: a
|
|
548
|
+
* supplied member (Guard's `may`) whose value, as the store shows it, forbids
|
|
549
|
+
* what the write does to the row. None where nothing held says so: an
|
|
550
|
+
* insert, a row not held, or a member not held or not refusing.
|
|
551
|
+
*/
|
|
552
|
+
const forbiddenByWrite = (write, input, keys, store) => {
|
|
553
|
+
const entity = write.input.entity;
|
|
554
|
+
const forbidders = forbiddersOf(entity);
|
|
555
|
+
if (forbidders.length === 0) return Option.none();
|
|
556
|
+
const bound = Write.bind(write, input, keys);
|
|
557
|
+
const held = store[entityKey(entity.name, bound.id)];
|
|
558
|
+
if (held === void 0) return Option.none();
|
|
559
|
+
const surface = Match.valueTags(write, {
|
|
560
|
+
Insert: () => Option.none(),
|
|
561
|
+
Delete: () => Option.some({
|
|
562
|
+
kind: "Delete",
|
|
563
|
+
members: [],
|
|
564
|
+
relations: []
|
|
565
|
+
}),
|
|
566
|
+
Update: () => Option.some({
|
|
567
|
+
kind: "Update",
|
|
568
|
+
members: [
|
|
569
|
+
...Object.keys(bound.values),
|
|
570
|
+
...Object.keys(bound.increments),
|
|
571
|
+
...Object.keys(bound.assignments)
|
|
572
|
+
],
|
|
573
|
+
relations: Object.entries(bound.links).map(([relation, link]) => ({
|
|
574
|
+
relation,
|
|
575
|
+
interaction: Option.isSome(link) ? "Link" : "Unlink"
|
|
576
|
+
}))
|
|
577
|
+
})
|
|
578
|
+
});
|
|
579
|
+
return Option.flatMap(surface, (asked) => Option.firstSomeOf(forbidders.map(([key, forbids]) => held.present.has(key) ? forbids(held.values[key], asked) : Option.none())));
|
|
580
|
+
};
|
|
581
|
+
const Mutation = {
|
|
582
|
+
/**
|
|
583
|
+
* Declares a mutation. `Input` and `Output` are codecs, or the fields of the
|
|
584
|
+
* `Schema.Struct` they would be (`{ id: ProjectId, name: Schema.String }`).
|
|
585
|
+
* `Refusal`, when given, is what the server may refuse with: a codec, usually
|
|
586
|
+
* a union of `Refusal.field(...)` and `Refusal.conflict`.
|
|
587
|
+
*/
|
|
588
|
+
/**
|
|
589
|
+
* A mutation that is a declared `Write` (`update`, `insert` or `delete`): its
|
|
590
|
+
* input is the write's, and it answers nothing but the row it changed, as a
|
|
591
|
+
* patch, or its deletion. An insert may be refused as taken, when a row
|
|
592
|
+
* already holds its id; an update or a delete as a conflict, when it
|
|
593
|
+
* `expect`s a revision the row has moved past, and as unmet, when it has a
|
|
594
|
+
* `when`; and any as invalid, when the row it would leave breaks an invariant.
|
|
595
|
+
*/
|
|
596
|
+
write: (name, write) => ({
|
|
597
|
+
name,
|
|
598
|
+
Input: write.input.schema,
|
|
599
|
+
Output: Schema.Struct({}),
|
|
600
|
+
Refusal: refusalOfWrite(write),
|
|
601
|
+
write
|
|
602
|
+
}),
|
|
603
|
+
make: (name, config) => ({
|
|
604
|
+
name,
|
|
605
|
+
Input: schemaOf(config.Input),
|
|
606
|
+
Output: schemaOf(config.Output),
|
|
607
|
+
Refusal: config.Refusal ?? Schema.Never
|
|
608
|
+
})
|
|
609
|
+
};
|
|
610
|
+
export { DerivedBy, Mutation, Refusal, beginMutation, brokenByWrite, derivedOfWrite, emptyMutationState, failMutation, forbiddenByWrite, forgetFailure, mutationStatus, optimisticOfWrite, patchOfWrite, predictionOfWrite, reconcileMutation, schemaOf, settledLater };
|
|
611
|
+
|
|
612
|
+
//# sourceMappingURL=mutation.js.map
|