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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbox-DNuc4kZp.d.ts","names":["_$__requirement_js0.RelationRequirement"],"sources":["../src/outbox.ts"],"mappings":";;;;;;KAmBY;WACG;;WACA;WAAyB,OAAO;;;;;;;cAsDlC,aAAW,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAiBZ,qBAAqB,YAAY;;UAiB5B;WACN,SAAS;WACT,MAAM;;cAGJ,mBAAmB;;cAUnB,WAAQ,MACb,uBAAqB,SAClB,0BACR;;;;;;;cAaU,cAAW,OAAW,cAAY,MAAQ,iBAAe;;cAgBzD,aAAU,aACV,kBAEV,OAAO,OAAO,cAAc,cAAc,oBAAoB,cAAc;;;;;;cAkBlE,gBAAa,aACb,kBAEV,OAAO,gBAAgB,cAAc,oBAAoB,cAAc;cAW7D,aAAU,QAAY,cAAY;;cAIlC,iBAAc,iBAAmB;;;;;cAUjC,YAAS,QAAY,cAAY;;KAUlC,SAAS,KAAK;;EAExB;aACW,OAAO;;aAEP;;aAEA;;aAEA,SAAS,OAAO,OAAO;;;EAGlC;aACW;aACA;;aAEA;aACA;aACA;;;cAGA,QAAM,KAAA,WAAA;;WAlBC,OAAA;WAEC;WAEC;WAEA,SAAA,OAAO,OAAO;;;WAIZ;WACD;WAEL;WACG;WACA;;;UAMJ;;WAEN,SAAS,OAAO;;WAEhB,KAAK,OAAO;;;;;WAKZ,OAAO,OAAO;;WAEd,OAAO,cAAc;;WAErB,MAAM;;cAGJ,aAAa;cAQb,cAAW;;WA7CJ,OAAA;WAEC;WAEC;WAEA,SAAA,OAAO,OAAO;;;WAIZ;WACD;WAEL;WACG;WACA;;cAmCR,aAAU;;WAlDH,OAAA;WAEC;WAEC;WAEA,SAAA,OAAO,OAAO;;;WAIZ;WACD;WAEL;WACG;WACA;;;cA8CR,WAAQ,QAAY,gBAAc;;cAQlC,YAAS,QAAY;;cAGrB,SAAM,QAAY,gBAAc,OAAO,OAAO;;;;;KAa/C;WACG;WAA2B,OAAO;WAAsB;;WAExD;WACA;WACA;WACA;WACA;;;cAIF,cAAW,QACd,cAAY,UAElB,qBACG,OAAO,QAAQ,mBAAmB,OAAO;WAAkB;QAC/D,cAAc;;UAqBA;WACN;;WAEA,UAAU,OAAO;WACjB,OAAO;;KAGN,kBAAkB,KAAK;;EAEjC;aAAoB;aAA0B,OAAO,OAAO,OAAO;;;EAEnE;aAAmB,OAAO;;;EAE1B;aAAuB;;;cAEZ,iBAAe,KAAA,WAAA;;WAJA,OAAA;;;WAEK;;;WAJF;WAAwB,OAAA,OAAO,OAAO;;;UASpD;WACN;WACA;;WAEA,OAAO,cAAc;;cAKnB,eAAY,QAAY,gBAAc;;;;;cAgCtC,aAAW,QAAA,UAAA,OAAA,OAAA;;;;;cASX,WAAY,GAAG,GAAG,GAAC,cAClB,QACJ,OAAO,OAAO,GAAG,GAAG,OAC3B,OAAO,OAAO,GAAG,GAAG;;cAgCV,aAAU,gBAAkB,OAAO,OAAO;;cAM1C,WAAQ;;cAGR,YAAS;;UAGL;;WAEN,OAAO,SAAS;;WAEhB,MAAM,SAAS;;;cAIb,UAAO,iBACH,UACN,iBACR,OAAO,OAAO,SAAS;;;;;;cAYb,aAAa,OAAO"}
|
package/dist/outbox.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { A as storedOf, C as readEntries, D as settledIn, E as sendLock, M as withDone, N as withLock, O as storeLock, S as outboxStatus, T as revisionOf, _ as headOf, a as OutboxStatus, b as operationOf, c as ReadEntry, d as StoredWrite, f as allStored, g as emptyStoredOutbox, h as emptyOutbox, i as OutboxState, j as storedText, k as storedFromText, l as RetryOptions, m as channelFor, n as OutboxItemState, o as Queued, p as backoff, r as OutboxLocks, s as QueuedState, t as OutboxItem, u as StoredOutbox, v as mergeStored, w as readStored, x as othersWaiting, y as onlineAgain } from "./outbox-DNuc4kZp.js";
|
|
2
|
+
export { OutboxItem, OutboxItemState, OutboxLocks, OutboxState, OutboxStatus, Queued, QueuedState, ReadEntry, RetryOptions, StoredOutbox, StoredWrite, allStored, backoff, channelFor, emptyOutbox, emptyStoredOutbox, headOf, mergeStored, onlineAgain, operationOf, othersWaiting, outboxStatus, readEntries, readStored, revisionOf, sendLock, settledIn, storeLock, storedFromText, storedOf, storedText, withDone, withLock };
|
package/dist/outbox.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { RelationRequest } from "./wire.js";
|
|
2
|
+
import { remoteErrorSchema } from "./remoteData.js";
|
|
3
|
+
import { Context, Data, Duration, Effect, Option, Random, Schema } from "effect";
|
|
4
|
+
import { KeyValueStore } from "effect/persistence";
|
|
5
|
+
// src/outbox.ts
|
|
6
|
+
/**
|
|
7
|
+
* The durable outbox (#247, operations phase 7): a write made with
|
|
8
|
+
* `Data.mutate(…, { durable: true })` is queued in the Model, copied to a
|
|
9
|
+
* `KeyValueStore`, and sent from there, in order, by one tab, so a reload
|
|
10
|
+
* keeps it, layer and all, and sends it again with the same operation id and
|
|
11
|
+
* the same `now`. Remote sends it later; Sync converges, which this does not.
|
|
12
|
+
*
|
|
13
|
+
* The Model's queue is the truth for this page; the store is its copy, shared
|
|
14
|
+
* by every tab on the same key, and written as a merge so no tab's write
|
|
15
|
+
* overwrites another's.
|
|
16
|
+
*/
|
|
17
|
+
const QueuedStateSchema = Schema.Union([Schema.Struct({ _tag: Schema.Literal("Waiting") }), Schema.Struct({
|
|
18
|
+
_tag: Schema.Literal("Failed"),
|
|
19
|
+
error: remoteErrorSchema
|
|
20
|
+
})]);
|
|
21
|
+
const CollationSchema = Schema.Union([
|
|
22
|
+
Schema.Struct({ _tag: Schema.Literal("Binary") }),
|
|
23
|
+
Schema.Struct({ _tag: Schema.Literal("AsciiFold") }),
|
|
24
|
+
Schema.Struct({
|
|
25
|
+
_tag: Schema.Literal("Locale"),
|
|
26
|
+
locale: Schema.String
|
|
27
|
+
})
|
|
28
|
+
]);
|
|
29
|
+
const ComputationSchema = Schema.Union([
|
|
30
|
+
Schema.Struct({
|
|
31
|
+
_tag: Schema.Literal("Field"),
|
|
32
|
+
key: Schema.String
|
|
33
|
+
}),
|
|
34
|
+
Schema.Struct({
|
|
35
|
+
_tag: Schema.Literal("Value"),
|
|
36
|
+
value: Schema.Unknown
|
|
37
|
+
}),
|
|
38
|
+
Schema.Struct({
|
|
39
|
+
_tag: Schema.Literal("Call"),
|
|
40
|
+
operator: Schema.String,
|
|
41
|
+
args: Schema.Array(Schema.suspend(() => ComputationSchema)),
|
|
42
|
+
collation: Schema.NullOr(CollationSchema)
|
|
43
|
+
})
|
|
44
|
+
]);
|
|
45
|
+
const Ref = Schema.Struct({
|
|
46
|
+
entity: Schema.String,
|
|
47
|
+
id: Schema.String
|
|
48
|
+
});
|
|
49
|
+
/** The layer a write showed, as stored: read back only where every operation still has its shape. */
|
|
50
|
+
const OptimisticSchema = Schema.Union([
|
|
51
|
+
Schema.Struct({
|
|
52
|
+
entity: Schema.String,
|
|
53
|
+
id: Schema.String,
|
|
54
|
+
values: Schema.Record(Schema.String, Schema.Unknown),
|
|
55
|
+
adds: Schema.optionalKey(Schema.Record(Schema.String, Schema.Number)),
|
|
56
|
+
computes: Schema.optionalKey(Schema.Record(Schema.String, ComputationSchema))
|
|
57
|
+
}),
|
|
58
|
+
Schema.Struct({
|
|
59
|
+
_tag: Schema.Literal("Delete"),
|
|
60
|
+
entity: Schema.String,
|
|
61
|
+
id: Schema.String
|
|
62
|
+
}),
|
|
63
|
+
Schema.Struct({
|
|
64
|
+
_tag: Schema.Literal("Insert"),
|
|
65
|
+
connection: Schema.String,
|
|
66
|
+
position: Schema.Literals(["prepend", "append"]),
|
|
67
|
+
edge: Schema.Struct({
|
|
68
|
+
key: Schema.String,
|
|
69
|
+
ref: Ref
|
|
70
|
+
})
|
|
71
|
+
}),
|
|
72
|
+
Schema.Struct({
|
|
73
|
+
_tag: Schema.Literal("Remove"),
|
|
74
|
+
connection: Schema.String,
|
|
75
|
+
edge: Schema.Struct({
|
|
76
|
+
key: Schema.String,
|
|
77
|
+
ref: Ref
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
]);
|
|
81
|
+
/**
|
|
82
|
+
* A durable write as the store keeps it: what a later page needs to show it
|
|
83
|
+
* and send it as itself. Its input is encoded by the Mutation's `Input`, so a
|
|
84
|
+
* page reads it back with the Mutation it has then.
|
|
85
|
+
*/
|
|
86
|
+
const StoredWrite = Schema.Struct({
|
|
87
|
+
/** Its operation id, which is also its request id. */
|
|
88
|
+
operation: Schema.String,
|
|
89
|
+
mutation: Schema.String,
|
|
90
|
+
input: Schema.Unknown,
|
|
91
|
+
keys: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
92
|
+
now: Schema.optionalKey(Schema.Number),
|
|
93
|
+
/** The layer it shows until it settles. */
|
|
94
|
+
optimistic: Schema.Array(OptimisticSchema),
|
|
95
|
+
/** Who its layer was predicted for. */
|
|
96
|
+
viewer: Schema.optionalKey(Ref),
|
|
97
|
+
/** What the reader was shown when they acted, for the answer to carry. */
|
|
98
|
+
shown: Schema.Array(RelationRequest),
|
|
99
|
+
/** Moves on each change of `state`; of two copies, the higher wins. */
|
|
100
|
+
revision: Schema.Number,
|
|
101
|
+
state: QueuedStateSchema
|
|
102
|
+
});
|
|
103
|
+
/** What makes a stored entry an operation at all: one without these is dropped. */
|
|
104
|
+
const OperationKey = Schema.Struct({
|
|
105
|
+
operation: Schema.String,
|
|
106
|
+
revision: Schema.Number
|
|
107
|
+
});
|
|
108
|
+
const keyOf = Schema.decodeUnknownOption(OperationKey);
|
|
109
|
+
const OUTBOX_VERSION = 1;
|
|
110
|
+
/** The record one key holds: every tab's queued writes, and the operations settled lately. */
|
|
111
|
+
const StoredOutboxSchema = Schema.Struct({
|
|
112
|
+
version: Schema.Literal(OUTBOX_VERSION),
|
|
113
|
+
scope: Schema.String,
|
|
114
|
+
entries: Schema.Array(Schema.Unknown),
|
|
115
|
+
done: Schema.Array(Schema.String)
|
|
116
|
+
});
|
|
117
|
+
const emptyStoredOutbox = {
|
|
118
|
+
entries: [],
|
|
119
|
+
done: []
|
|
120
|
+
};
|
|
121
|
+
/** `done` with `settled` after it, the newest `DONE_WINDOW`, each once. */
|
|
122
|
+
const withDone = (done, settled) => {
|
|
123
|
+
if (settled.every((operation) => done.includes(operation))) return done;
|
|
124
|
+
const kept = new Set(done);
|
|
125
|
+
for (const operation of settled) kept.add(operation);
|
|
126
|
+
return [...kept].slice(-256);
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Two copies of a queue as one: entries of both by operation, none that
|
|
130
|
+
* either settled, the higher revision of each, in the store's order and then
|
|
131
|
+
* mine. A join, so tabs that save at once lose nothing the next save does
|
|
132
|
+
* not put back.
|
|
133
|
+
*/
|
|
134
|
+
const mergeStored = (store, mine) => {
|
|
135
|
+
const done = withDone(store.done, mine.done);
|
|
136
|
+
const gone = new Set(done);
|
|
137
|
+
const kept = /* @__PURE__ */ new Map();
|
|
138
|
+
for (const raw of [...store.entries, ...mine.entries]) {
|
|
139
|
+
const key = keyOf(raw);
|
|
140
|
+
if (Option.isNone(key) || gone.has(key.value.operation)) continue;
|
|
141
|
+
const held = kept.get(key.value.operation);
|
|
142
|
+
if (held === void 0 || held.revision < key.value.revision) kept.set(key.value.operation, {
|
|
143
|
+
raw,
|
|
144
|
+
revision: key.value.revision
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
entries: [...kept.values()].map(({ raw }) => raw),
|
|
149
|
+
done
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
/** The record under `key` for `scope`: none when absent, of another scope or version, or not JSON. */
|
|
153
|
+
const readStored = (key, scope) => Effect.gen(function* () {
|
|
154
|
+
const text = yield* (yield* KeyValueStore.KeyValueStore).get(key);
|
|
155
|
+
if (text === void 0) return emptyStoredOutbox;
|
|
156
|
+
const parsed = Schema.decodeUnknownOption(Schema.fromJsonString(StoredOutboxSchema))(text);
|
|
157
|
+
return Option.match(parsed, {
|
|
158
|
+
onNone: () => emptyStoredOutbox,
|
|
159
|
+
onSome: (record) => record.scope === scope ? {
|
|
160
|
+
entries: record.entries,
|
|
161
|
+
done: record.done
|
|
162
|
+
} : emptyStoredOutbox
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
/**
|
|
166
|
+
* Whether the record under `key` holds writes waiting for another scope than
|
|
167
|
+
* `scope`: a key that is not one per principal. Writing over it would lose
|
|
168
|
+
* them, so it is not written.
|
|
169
|
+
*/
|
|
170
|
+
const othersWaiting = (key, scope) => Effect.gen(function* () {
|
|
171
|
+
const text = yield* (yield* KeyValueStore.KeyValueStore).get(key);
|
|
172
|
+
return Option.exists(Option.flatMap(Option.fromUndefinedOr(text), (stored) => Schema.decodeUnknownOption(Schema.fromJsonString(StoredOutboxSchema))(stored)), (record) => record.scope !== scope && record.entries.length > 0);
|
|
173
|
+
});
|
|
174
|
+
const storedText = (outbox, scope) => JSON.stringify({
|
|
175
|
+
version: OUTBOX_VERSION,
|
|
176
|
+
scope,
|
|
177
|
+
entries: outbox.entries,
|
|
178
|
+
done: outbox.done
|
|
179
|
+
});
|
|
180
|
+
/** A queue's text, as `storedText` wrote it, read back. */
|
|
181
|
+
const storedFromText = (text) => Option.match(Schema.decodeUnknownOption(Schema.fromJsonString(StoredOutboxSchema))(text), {
|
|
182
|
+
onNone: () => emptyStoredOutbox,
|
|
183
|
+
onSome: ({ entries, done }) => ({
|
|
184
|
+
entries,
|
|
185
|
+
done
|
|
186
|
+
})
|
|
187
|
+
});
|
|
188
|
+
/**
|
|
189
|
+
* Whether the store says `operation` is settled: done, or refused. A store
|
|
190
|
+
* that lacks it says nothing (it could not be written), and it is sent.
|
|
191
|
+
*/
|
|
192
|
+
const settledIn = (outbox, operation) => outbox.done.includes(operation) || outbox.entries.some((raw) => Option.exists(Schema.decodeUnknownOption(StoredWrite)(raw), (write) => write.operation === operation && write.state._tag !== "Waiting"));
|
|
193
|
+
const Queued = Data.taggedEnum();
|
|
194
|
+
const emptyOutbox = {
|
|
195
|
+
session: Option.none(),
|
|
196
|
+
key: Option.none(),
|
|
197
|
+
scope: Option.none(),
|
|
198
|
+
queue: [],
|
|
199
|
+
done: []
|
|
200
|
+
};
|
|
201
|
+
const operationOf = Queued.$match({
|
|
202
|
+
Readable: ({ write }) => write.operation,
|
|
203
|
+
Unreadable: ({ operation }) => operation
|
|
204
|
+
});
|
|
205
|
+
const revisionOf = Queued.$match({
|
|
206
|
+
Readable: ({ write }) => write.revision,
|
|
207
|
+
Unreadable: ({ revision }) => revision
|
|
208
|
+
});
|
|
209
|
+
const isStored = Queued.$match({
|
|
210
|
+
Readable: ({ stored }) => stored,
|
|
211
|
+
Unreadable: ({ stored }) => stored
|
|
212
|
+
});
|
|
213
|
+
/** The queue as the store keeps it. */
|
|
214
|
+
const storedOf = (outbox) => ({
|
|
215
|
+
entries: outbox.queue.map(Queued.$match({
|
|
216
|
+
Readable: ({ write }) => write,
|
|
217
|
+
Unreadable: ({ raw }) => raw
|
|
218
|
+
})),
|
|
219
|
+
done: outbox.done
|
|
220
|
+
});
|
|
221
|
+
/** Whether every queued write is in the store as the Model holds it. */
|
|
222
|
+
const allStored = (outbox) => outbox.queue.every(isStored);
|
|
223
|
+
/** The write sent next: the oldest one waiting that the store holds. */
|
|
224
|
+
const headOf = (outbox) => {
|
|
225
|
+
for (const queued of outbox.queue) if (queued._tag === "Readable" && queued.write.state._tag === "Waiting" && queued.stored) return Option.some(queued.write);
|
|
226
|
+
return Option.none();
|
|
227
|
+
};
|
|
228
|
+
/** A stored queue as a page reads it, entry by entry: one bad entry never fails the rest. */
|
|
229
|
+
const readEntries = (outbox, inputOf) => outbox.entries.flatMap((raw) => {
|
|
230
|
+
const key = keyOf(raw);
|
|
231
|
+
if (Option.isNone(key)) return [];
|
|
232
|
+
const unreadable = (reason) => [{
|
|
233
|
+
_tag: "Unreadable",
|
|
234
|
+
...key.value,
|
|
235
|
+
raw,
|
|
236
|
+
reason
|
|
237
|
+
}];
|
|
238
|
+
const write = Schema.decodeUnknownOption(StoredWrite)(raw);
|
|
239
|
+
if (Option.isNone(write)) return unreadable("it was stored in a shape this version cannot read");
|
|
240
|
+
return Option.match(inputOf(write.value.mutation), {
|
|
241
|
+
onNone: () => unreadable(`${write.value.mutation} is not a mutation this version registers`),
|
|
242
|
+
onSome: (decode) => Option.match(decode(write.value.input), {
|
|
243
|
+
onNone: () => unreadable(`its input no longer decodes as ${write.value.mutation}'s`),
|
|
244
|
+
onSome: ({ decoded }) => [{
|
|
245
|
+
_tag: "Readable",
|
|
246
|
+
write: write.value,
|
|
247
|
+
input: decoded
|
|
248
|
+
}]
|
|
249
|
+
})
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
const OutboxItemState = Data.taggedEnum();
|
|
253
|
+
const MutationName = Schema.Struct({ mutation: Schema.String });
|
|
254
|
+
const outboxStatus = (outbox) => {
|
|
255
|
+
const items = outbox.queue.map(Queued.$match({
|
|
256
|
+
Readable: ({ write, attempt, delayed }) => ({
|
|
257
|
+
requestId: write.operation,
|
|
258
|
+
mutation: Option.some(write.mutation),
|
|
259
|
+
state: write.state._tag === "Waiting" ? OutboxItemState.Waiting({
|
|
260
|
+
attempt,
|
|
261
|
+
error: delayed
|
|
262
|
+
}) : OutboxItemState.Failed({ error: write.state.error })
|
|
263
|
+
}),
|
|
264
|
+
Unreadable: ({ operation, raw, reason }) => ({
|
|
265
|
+
requestId: operation,
|
|
266
|
+
mutation: Option.map(Schema.decodeUnknownOption(MutationName)(raw), ({ mutation }) => mutation),
|
|
267
|
+
state: OutboxItemState.Unreadable({ reason })
|
|
268
|
+
})
|
|
269
|
+
}));
|
|
270
|
+
return {
|
|
271
|
+
waiting: items.filter((item) => item.state._tag === "Waiting").length,
|
|
272
|
+
failed: items.filter((item) => item.state._tag === "Failed").length,
|
|
273
|
+
items
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* Where the outbox takes its Web Locks: the browser's, where it has them. A
|
|
278
|
+
* trace, which is one tab, takes none.
|
|
279
|
+
*/
|
|
280
|
+
const OutboxLocks = Context.Reference("foldkit-remote/OutboxLocks", { defaultValue: () => Option.fromUndefinedOr(globalThis.navigator?.locks) });
|
|
281
|
+
/**
|
|
282
|
+
* Holds the Web Lock `name` while `effect` runs, where there are Web Locks;
|
|
283
|
+
* elsewhere runs it as it is (one tab, or a server).
|
|
284
|
+
*/
|
|
285
|
+
const withLock = (name, effect) => Effect.flatMap(Effect.service(OutboxLocks), (held) => Option.match(held, {
|
|
286
|
+
onNone: () => effect,
|
|
287
|
+
onSome: (locks) => heldWhile(locks, name, effect)
|
|
288
|
+
}));
|
|
289
|
+
const heldWhile = (locks, name, effect) => Effect.acquireUseRelease(Effect.callback((resume, signal) => {
|
|
290
|
+
locks.request(name, { signal }, () => {
|
|
291
|
+
let release = () => void 0;
|
|
292
|
+
const held = new Promise((done) => {
|
|
293
|
+
release = done;
|
|
294
|
+
});
|
|
295
|
+
resume(Effect.succeed(release));
|
|
296
|
+
return held;
|
|
297
|
+
}).catch(() => void 0);
|
|
298
|
+
}), () => effect, (release) => Effect.sync(release));
|
|
299
|
+
/** The `BroadcastChannel` the tabs on one key speak on; none where there is none. */
|
|
300
|
+
const channelFor = (key) => typeof globalThis.BroadcastChannel === "function" ? Option.some(new globalThis.BroadcastChannel(`foldkit-remote-outbox:${key}`)) : Option.none();
|
|
301
|
+
/** The lock the tabs on one key send under: one sends, the others wait. */
|
|
302
|
+
const sendLock = (key) => `foldkit-remote-outbox:${key}:send`;
|
|
303
|
+
/** The lock a write to the store is made under, so two tabs never write it at once. */
|
|
304
|
+
const storeLock = (key) => `foldkit-remote-outbox:${key}:store`;
|
|
305
|
+
/** `base` doubled per attempt, up to `max`, ±20% so tabs that failed together do not retry together. */
|
|
306
|
+
const backoff = (attempt, options = {}) => {
|
|
307
|
+
const base = Duration.toMillis(Duration.fromInputUnsafe(options.base ?? "1 second"));
|
|
308
|
+
const max = Duration.toMillis(Duration.fromInputUnsafe(options.max ?? "30 seconds"));
|
|
309
|
+
const wait = Math.min(max, base * 2 ** Math.max(0, attempt - 1));
|
|
310
|
+
return Effect.map(Random.nextBetween(.8, 1.2), (jitter) => Duration.millis(wait * jitter));
|
|
311
|
+
};
|
|
312
|
+
/**
|
|
313
|
+
* Waits until the browser says it is online again, where it can say so;
|
|
314
|
+
* never elsewhere. A wait for a retry races it, so a write goes as soon as
|
|
315
|
+
* the network is back.
|
|
316
|
+
*/
|
|
317
|
+
const onlineAgain = Effect.callback((resume) => {
|
|
318
|
+
const target = globalThis.window;
|
|
319
|
+
if (target === void 0 || typeof target.addEventListener !== "function") return;
|
|
320
|
+
const online = () => resume(Effect.void);
|
|
321
|
+
target.addEventListener("online", online, { once: true });
|
|
322
|
+
return Effect.sync(() => target.removeEventListener("online", online));
|
|
323
|
+
});
|
|
324
|
+
export { OutboxItemState, OutboxLocks, Queued, StoredWrite, allStored, backoff, channelFor, emptyOutbox, emptyStoredOutbox, headOf, mergeStored, onlineAgain, operationOf, othersWaiting, outboxStatus, readEntries, readStored, revisionOf, sendLock, settledIn, storeLock, storedFromText, storedOf, storedText, withDone, withLock };
|
|
325
|
+
|
|
326
|
+
//# sourceMappingURL=outbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbox.js","names":[],"sources":["../src/outbox.ts"],"sourcesContent":["/**\n * The durable outbox (#247, operations phase 7): a write made with\n * `Data.mutate(…, { durable: true })` is queued in the Model, copied to a\n * `KeyValueStore`, and sent from there, in order, by one tab, so a reload\n * keeps it, layer and all, and sends it again with the same operation id and\n * the same `now`. Remote sends it later; Sync converges, which this does not.\n *\n * The Model's queue is the truth for this page; the store is its copy, shared\n * by every tab on the same key, and written as a merge so no tab's write\n * overwrites another's.\n */\nimport { Context, Data, Duration, Effect, Option, Random, Schema } from 'effect'\nimport { KeyValueStore } from 'effect/persistence'\nimport type { Computation } from 'foldkit-entity'\nimport type { OptimisticOperation } from './optimistic.js'\nimport { remoteErrorSchema, type RemoteError } from './remoteData.js'\nimport { RelationRequest } from './wire.js'\n\n/** Where a queued write stands, as the store keeps it. */\nexport type QueuedState =\n | { readonly _tag: 'Waiting' }\n | { readonly _tag: 'Failed'; readonly error: RemoteError }\n\nconst QueuedStateSchema = Schema.Union([\n Schema.Struct({ _tag: Schema.Literal('Waiting') }),\n Schema.Struct({ _tag: Schema.Literal('Failed'), error: remoteErrorSchema }),\n])\n\nconst CollationSchema = Schema.Union([\n Schema.Struct({ _tag: Schema.Literal('Binary') }),\n Schema.Struct({ _tag: Schema.Literal('AsciiFold') }),\n Schema.Struct({ _tag: Schema.Literal('Locale'), locale: Schema.String }),\n])\n\nconst ComputationSchema: Schema.Codec<Computation> = Schema.Union([\n Schema.Struct({ _tag: Schema.Literal('Field'), key: Schema.String }),\n Schema.Struct({ _tag: Schema.Literal('Value'), value: Schema.Unknown }),\n Schema.Struct({\n _tag: Schema.Literal('Call'),\n operator: Schema.String,\n args: Schema.Array(Schema.suspend((): Schema.Codec<Computation> => ComputationSchema)),\n collation: Schema.NullOr(CollationSchema),\n }),\n])\n\nconst Ref = Schema.Struct({ entity: Schema.String, id: Schema.String })\n\n/** The layer a write showed, as stored: read back only where every operation still has its shape. */\nconst OptimisticSchema: Schema.Codec<OptimisticOperation> = Schema.Union([\n Schema.Struct({\n entity: Schema.String,\n id: Schema.String,\n values: Schema.Record(Schema.String, Schema.Unknown),\n adds: Schema.optionalKey(Schema.Record(Schema.String, Schema.Number)),\n computes: Schema.optionalKey(Schema.Record(Schema.String, ComputationSchema)),\n }),\n Schema.Struct({ _tag: Schema.Literal('Delete'), entity: Schema.String, id: Schema.String }),\n Schema.Struct({\n _tag: Schema.Literal('Insert'),\n connection: Schema.String,\n position: Schema.Literals(['prepend', 'append']),\n edge: Schema.Struct({ key: Schema.String, ref: Ref }),\n }),\n Schema.Struct({\n _tag: Schema.Literal('Remove'),\n connection: Schema.String,\n edge: Schema.Struct({ key: Schema.String, ref: Ref }),\n }),\n])\n\n/**\n * A durable write as the store keeps it: what a later page needs to show it\n * and send it as itself. Its input is encoded by the Mutation's `Input`, so a\n * page reads it back with the Mutation it has then.\n */\nexport const StoredWrite = Schema.Struct({\n /** Its operation id, which is also its request id. */\n operation: Schema.String,\n mutation: Schema.String,\n input: Schema.Unknown,\n keys: Schema.optionalKey(Schema.Array(Schema.String)),\n now: Schema.optionalKey(Schema.Number),\n /** The layer it shows until it settles. */\n optimistic: Schema.Array(OptimisticSchema),\n /** Who its layer was predicted for. */\n viewer: Schema.optionalKey(Ref),\n /** What the reader was shown when they acted, for the answer to carry. */\n shown: Schema.Array(RelationRequest),\n /** Moves on each change of `state`; of two copies, the higher wins. */\n revision: Schema.Number,\n state: QueuedStateSchema,\n})\nexport type StoredWrite = typeof StoredWrite.Type\n\n/** What makes a stored entry an operation at all: one without these is dropped. */\nconst OperationKey = Schema.Struct({ operation: Schema.String, revision: Schema.Number })\nconst keyOf = Schema.decodeUnknownOption(OperationKey)\n\nconst OUTBOX_VERSION = 1\n\n/** The record one key holds: every tab's queued writes, and the operations settled lately. */\nconst StoredOutboxSchema = Schema.Struct({\n version: Schema.Literal(OUTBOX_VERSION),\n scope: Schema.String,\n entries: Schema.Array(Schema.Unknown),\n done: Schema.Array(Schema.String),\n})\n\n/** A key's queue as stored, each entry as it was written, read or not. */\nexport interface StoredOutbox {\n readonly entries: ReadonlyArray<unknown>\n readonly done: ReadonlyArray<string>\n}\n\nexport const emptyStoredOutbox: StoredOutbox = { entries: [], done: [] }\n\n/**\n * How many settled operation ids are kept. A copy of a write settled longer\n * ago than this could come back from a tab that slept through it; the\n * server's ledger answers it again from its record.\n */\nconst DONE_WINDOW = 256\n\n/** `done` with `settled` after it, the newest `DONE_WINDOW`, each once. */\nexport const withDone = (\n done: ReadonlyArray<string>,\n settled: ReadonlyArray<string>,\n): ReadonlyArray<string> => {\n if (settled.every(operation => done.includes(operation))) return done\n const kept = new Set(done)\n for (const operation of settled) kept.add(operation)\n return [...kept].slice(-DONE_WINDOW)\n}\n\n/**\n * Two copies of a queue as one: entries of both by operation, none that\n * either settled, the higher revision of each, in the store's order and then\n * mine. A join, so tabs that save at once lose nothing the next save does\n * not put back.\n */\nexport const mergeStored = (store: StoredOutbox, mine: StoredOutbox): StoredOutbox => {\n const done = withDone(store.done, mine.done)\n const gone = new Set(done)\n const kept = new Map<string, { readonly raw: unknown; readonly revision: number }>()\n for (const raw of [...store.entries, ...mine.entries]) {\n const key = keyOf(raw)\n if (Option.isNone(key) || gone.has(key.value.operation)) continue\n const held = kept.get(key.value.operation)\n if (held === undefined || held.revision < key.value.revision) {\n kept.set(key.value.operation, { raw, revision: key.value.revision })\n }\n }\n return { entries: [...kept.values()].map(({ raw }) => raw), done }\n}\n\n/** The record under `key` for `scope`: none when absent, of another scope or version, or not JSON. */\nexport const readStored = (\n key: string,\n scope: string,\n): Effect.Effect<StoredOutbox, KeyValueStore.KeyValueStoreError, KeyValueStore.KeyValueStore> =>\n Effect.gen(function* () {\n const kv = yield* KeyValueStore.KeyValueStore\n const text = yield* kv.get(key)\n if (text === undefined) return emptyStoredOutbox\n const parsed = Schema.decodeUnknownOption(Schema.fromJsonString(StoredOutboxSchema))(text)\n return Option.match(parsed, {\n onNone: () => emptyStoredOutbox,\n onSome: record =>\n record.scope === scope ? { entries: record.entries, done: record.done } : emptyStoredOutbox,\n })\n })\n\n/**\n * Whether the record under `key` holds writes waiting for another scope than\n * `scope`: a key that is not one per principal. Writing over it would lose\n * them, so it is not written.\n */\nexport const othersWaiting = (\n key: string,\n scope: string,\n): Effect.Effect<boolean, KeyValueStore.KeyValueStoreError, KeyValueStore.KeyValueStore> =>\n Effect.gen(function* () {\n const text = yield* (yield* KeyValueStore.KeyValueStore).get(key)\n return Option.exists(\n Option.flatMap(Option.fromUndefinedOr(text), stored =>\n Schema.decodeUnknownOption(Schema.fromJsonString(StoredOutboxSchema))(stored),\n ),\n record => record.scope !== scope && record.entries.length > 0,\n )\n })\n\nexport const storedText = (outbox: StoredOutbox, scope: string): string =>\n JSON.stringify({ version: OUTBOX_VERSION, scope, entries: outbox.entries, done: outbox.done })\n\n/** A queue's text, as `storedText` wrote it, read back. */\nexport const storedFromText = (text: string): StoredOutbox =>\n Option.match(Schema.decodeUnknownOption(Schema.fromJsonString(StoredOutboxSchema))(text), {\n onNone: () => emptyStoredOutbox,\n onSome: ({ entries, done }) => ({ entries, done }),\n })\n\n/**\n * Whether the store says `operation` is settled: done, or refused. A store\n * that lacks it says nothing (it could not be written), and it is sent.\n */\nexport const settledIn = (outbox: StoredOutbox, operation: string): boolean =>\n outbox.done.includes(operation) ||\n outbox.entries.some(raw =>\n Option.exists(\n Schema.decodeUnknownOption(StoredWrite)(raw),\n write => write.operation === operation && write.state._tag !== 'Waiting',\n ),\n )\n\n/** A queued write in the Model. */\nexport type Queued = Data.TaggedEnum<{\n /** One this version can send: shown, in the ledger, and sent in its turn. */\n Readable: {\n readonly write: StoredWrite\n /** Whether the store is known to hold this revision: only then is it sent. */\n readonly stored: boolean\n /** Sends in a row that nothing answered, this page. */\n readonly attempt: number\n /** Why the last of them failed. */\n readonly delayed: Option.Option<RemoteError>\n }\n /** One this version cannot read (a Mutation it lacks, an input that no longer decodes): kept, never sent. */\n Unreadable: {\n readonly operation: string\n readonly revision: number\n /** The entry as it was stored, written back as it is. */\n readonly raw: unknown\n readonly reason: string\n readonly stored: boolean\n }\n}>\nexport const Queued = Data.taggedEnum<Queued>()\n\n/** The durable writes this page holds, and the store they are copied to. */\nexport interface OutboxState {\n /** Names this page load's operations; minted by the outbox's restore. */\n readonly session: Option.Option<string>\n /** The store key the queue was read from; it is saved there and nowhere else. */\n readonly key: Option.Option<string>\n /**\n * Whose queue it is, as it was read: a page whose scope moves on sends none\n * of it, and reads the new scope's instead.\n */\n readonly scope: Option.Option<string>\n /** Every tab's queued writes, in the order they are sent. */\n readonly queue: ReadonlyArray<Queued>\n /** Operations settled lately, here or in another tab. */\n readonly done: ReadonlyArray<string>\n}\n\nexport const emptyOutbox: OutboxState = {\n session: Option.none(),\n key: Option.none(),\n scope: Option.none(),\n queue: [],\n done: [],\n}\n\nexport const operationOf = Queued.$match({\n Readable: ({ write }) => write.operation,\n Unreadable: ({ operation }) => operation,\n})\n\nexport const revisionOf = Queued.$match({\n Readable: ({ write }) => write.revision,\n Unreadable: ({ revision }) => revision,\n})\n\nconst isStored = Queued.$match({\n Readable: ({ stored }) => stored,\n Unreadable: ({ stored }) => stored,\n})\n\n/** The queue as the store keeps it. */\nexport const storedOf = (outbox: OutboxState): StoredOutbox => ({\n entries: outbox.queue.map(\n Queued.$match({ Readable: ({ write }) => write, Unreadable: ({ raw }) => raw }),\n ),\n done: outbox.done,\n})\n\n/** Whether every queued write is in the store as the Model holds it. */\nexport const allStored = (outbox: OutboxState): boolean => outbox.queue.every(isStored)\n\n/** The write sent next: the oldest one waiting that the store holds. */\nexport const headOf = (outbox: OutboxState): Option.Option<StoredWrite> => {\n for (const queued of outbox.queue) {\n if (queued._tag === 'Readable' && queued.write.state._tag === 'Waiting' && queued.stored) {\n return Option.some(queued.write)\n }\n }\n return Option.none()\n}\n\n/**\n * A stored entry as a page reads it: a write it can send, with its input\n * decoded, or one it cannot, with why.\n */\nexport type ReadEntry =\n | { readonly _tag: 'Readable'; readonly write: StoredWrite; readonly input: unknown }\n | {\n readonly _tag: 'Unreadable'\n readonly operation: string\n readonly revision: number\n readonly raw: unknown\n readonly reason: string\n }\n\n/** A stored queue as a page reads it, entry by entry: one bad entry never fails the rest. */\nexport const readEntries = (\n outbox: StoredOutbox,\n inputOf: (\n mutation: string,\n ) => Option.Option<(input: unknown) => Option.Option<{ readonly decoded: unknown }>>,\n): ReadonlyArray<ReadEntry> =>\n outbox.entries.flatMap((raw): ReadonlyArray<ReadEntry> => {\n const key = keyOf(raw)\n // Not an operation at all: nothing to send, show, or dismiss.\n if (Option.isNone(key)) return []\n const unreadable = (reason: string): ReadonlyArray<ReadEntry> => [\n { _tag: 'Unreadable', ...key.value, raw, reason },\n ]\n const write = Schema.decodeUnknownOption(StoredWrite)(raw)\n if (Option.isNone(write)) return unreadable('it was stored in a shape this version cannot read')\n return Option.match(inputOf(write.value.mutation), {\n onNone: () => unreadable(`${write.value.mutation} is not a mutation this version registers`),\n onSome: decode =>\n Option.match(decode(write.value.input), {\n onNone: () => unreadable(`its input no longer decodes as ${write.value.mutation}'s`),\n onSome: ({ decoded }) => [{ _tag: 'Readable', write: write.value, input: decoded }],\n }),\n })\n })\n\n/** One item of `Data.outboxStatus`. */\nexport interface OutboxItem {\n readonly requestId: string\n /** The Mutation it names; none where the stored entry names none this version can read. */\n readonly mutation: Option.Option<string>\n readonly state: OutboxItemState\n}\n\nexport type OutboxItemState = Data.TaggedEnum<{\n /** Not answered yet: `attempt` sends nothing answered so far, `error` the last one's. */\n Waiting: { readonly attempt: number; readonly error: Option.Option<RemoteError> }\n /** The server answered no: `Data.retry` or `Data.dismiss` it. */\n Failed: { readonly error: RemoteError }\n /** Stored by a version of the app this one cannot read: `Data.dismiss` it. */\n Unreadable: { readonly reason: string }\n}>\nexport const OutboxItemState = Data.taggedEnum<OutboxItemState>()\n\n/** What the outbox holds, for a view: \"3 changes waiting\". */\nexport interface OutboxStatus {\n readonly waiting: number\n readonly failed: number\n /** Every durable write not yet settled, in the order they are sent. */\n readonly items: ReadonlyArray<OutboxItem>\n}\n\nconst MutationName = Schema.Struct({ mutation: Schema.String })\n\nexport const outboxStatus = (outbox: OutboxState): OutboxStatus => {\n const items = outbox.queue.map(\n Queued.$match({\n Readable: ({ write, attempt, delayed }): OutboxItem => ({\n requestId: write.operation,\n mutation: Option.some(write.mutation),\n state:\n write.state._tag === 'Waiting'\n ? OutboxItemState.Waiting({ attempt, error: delayed })\n : OutboxItemState.Failed({ error: write.state.error }),\n }),\n Unreadable: ({ operation, raw, reason }): OutboxItem => ({\n requestId: operation,\n mutation: Option.map(\n Schema.decodeUnknownOption(MutationName)(raw),\n ({ mutation }) => mutation,\n ),\n state: OutboxItemState.Unreadable({ reason }),\n }),\n }),\n )\n return {\n waiting: items.filter(item => item.state._tag === 'Waiting').length,\n failed: items.filter(item => item.state._tag === 'Failed').length,\n items,\n }\n}\n\n/**\n * Where the outbox takes its Web Locks: the browser's, where it has them. A\n * trace, which is one tab, takes none.\n */\nexport const OutboxLocks = Context.Reference<Option.Option<LockManager>>(\n 'foldkit-remote/OutboxLocks',\n { defaultValue: () => Option.fromUndefinedOr(globalThis.navigator?.locks) },\n)\n\n/**\n * Holds the Web Lock `name` while `effect` runs, where there are Web Locks;\n * elsewhere runs it as it is (one tab, or a server).\n */\nexport const withLock = <A, E, R>(\n name: string,\n effect: Effect.Effect<A, E, R>,\n): Effect.Effect<A, E, R> =>\n Effect.flatMap(Effect.service(OutboxLocks), held =>\n Option.match(held, {\n onNone: () => effect,\n onSome: locks => heldWhile(locks, name, effect),\n }),\n )\n\nconst heldWhile = <A, E, R>(\n locks: LockManager,\n name: string,\n effect: Effect.Effect<A, E, R>,\n): Effect.Effect<A, E, R> =>\n Effect.acquireUseRelease(\n Effect.callback<() => void>((resume, signal) => {\n locks\n .request(name, { signal }, () => {\n let release: () => void = () => undefined\n const held = new Promise<void>(done => {\n release = done\n })\n resume(Effect.succeed(release))\n return held\n })\n // An abandoned wait rejects with the signal's reason; nothing waits on it.\n .catch(() => undefined)\n }),\n () => effect,\n release => Effect.sync(release),\n )\n\n/** The `BroadcastChannel` the tabs on one key speak on; none where there is none. */\nexport const channelFor = (key: string): Option.Option<BroadcastChannel> =>\n typeof globalThis.BroadcastChannel === 'function'\n ? Option.some(new globalThis.BroadcastChannel(`foldkit-remote-outbox:${key}`))\n : Option.none()\n\n/** The lock the tabs on one key send under: one sends, the others wait. */\nexport const sendLock = (key: string): string => `foldkit-remote-outbox:${key}:send`\n\n/** The lock a write to the store is made under, so two tabs never write it at once. */\nexport const storeLock = (key: string): string => `foldkit-remote-outbox:${key}:store`\n\n/** How long the outbox waits before sending again after `attempt` sends nothing answered. */\nexport interface RetryOptions {\n /** The first wait; 1 second unless given. */\n readonly base?: Duration.Input | undefined\n /** The longest; 30 seconds unless given. */\n readonly max?: Duration.Input | undefined\n}\n\n/** `base` doubled per attempt, up to `max`, ±20% so tabs that failed together do not retry together. */\nexport const backoff = (\n attempt: number,\n options: RetryOptions = {},\n): Effect.Effect<Duration.Duration> => {\n const base = Duration.toMillis(Duration.fromInputUnsafe(options.base ?? '1 second'))\n const max = Duration.toMillis(Duration.fromInputUnsafe(options.max ?? '30 seconds'))\n const wait = Math.min(max, base * 2 ** Math.max(0, attempt - 1))\n return Effect.map(Random.nextBetween(0.8, 1.2), jitter => Duration.millis(wait * jitter))\n}\n\n/**\n * Waits until the browser says it is online again, where it can say so;\n * never elsewhere. A wait for a retry races it, so a write goes as soon as\n * the network is back.\n */\nexport const onlineAgain: Effect.Effect<void> = Effect.callback<void>(resume => {\n const target = globalThis.window\n if (target === undefined || typeof target.addEventListener !== 'function') return\n const online = () => resume(Effect.void)\n target.addEventListener('online', online, { once: true })\n return Effect.sync(() => target.removeEventListener('online', online))\n})\n"],"mappings":";;;;;;;;;;;;;;;;AAuBA,MAAM,oBAAoB,OAAO,MAAM,CACrC,OAAO,OAAO,EAAE,MAAM,OAAO,QAAQ,SAAS,EAAE,CAAC,GACjD,OAAO,OAAO;CAAE,MAAM,OAAO,QAAQ,QAAQ;CAAG,OAAO;AAAkB,CAAC,CAC5E,CAAC;AAED,MAAM,kBAAkB,OAAO,MAAM;CACnC,OAAO,OAAO,EAAE,MAAM,OAAO,QAAQ,QAAQ,EAAE,CAAC;CAChD,OAAO,OAAO,EAAE,MAAM,OAAO,QAAQ,WAAW,EAAE,CAAC;CACnD,OAAO,OAAO;EAAE,MAAM,OAAO,QAAQ,QAAQ;EAAG,QAAQ,OAAO;CAAO,CAAC;AACzE,CAAC;AAED,MAAM,oBAA+C,OAAO,MAAM;CAChE,OAAO,OAAO;EAAE,MAAM,OAAO,QAAQ,OAAO;EAAG,KAAK,OAAO;CAAO,CAAC;CACnE,OAAO,OAAO;EAAE,MAAM,OAAO,QAAQ,OAAO;EAAG,OAAO,OAAO;CAAQ,CAAC;CACtE,OAAO,OAAO;EACZ,MAAM,OAAO,QAAQ,MAAM;EAC3B,UAAU,OAAO;EACjB,MAAM,OAAO,MAAM,OAAO,cAAyC,iBAAiB,CAAC;EACrF,WAAW,OAAO,OAAO,eAAe;CAC1C,CAAC;AACH,CAAC;AAED,MAAM,MAAM,OAAO,OAAO;CAAE,QAAQ,OAAO;CAAQ,IAAI,OAAO;AAAO,CAAC;;AAGtE,MAAM,mBAAsD,OAAO,MAAM;CACvE,OAAO,OAAO;EACZ,QAAQ,OAAO;EACf,IAAI,OAAO;EACX,QAAQ,OAAO,OAAO,OAAO,QAAQ,OAAO,OAAO;EACnD,MAAM,OAAO,YAAY,OAAO,OAAO,OAAO,QAAQ,OAAO,MAAM,CAAC;EACpE,UAAU,OAAO,YAAY,OAAO,OAAO,OAAO,QAAQ,iBAAiB,CAAC;CAC9E,CAAC;CACD,OAAO,OAAO;EAAE,MAAM,OAAO,QAAQ,QAAQ;EAAG,QAAQ,OAAO;EAAQ,IAAI,OAAO;CAAO,CAAC;CAC1F,OAAO,OAAO;EACZ,MAAM,OAAO,QAAQ,QAAQ;EAC7B,YAAY,OAAO;EACnB,UAAU,OAAO,SAAS,CAAC,WAAW,QAAQ,CAAC;EAC/C,MAAM,OAAO,OAAO;GAAE,KAAK,OAAO;GAAQ,KAAK;EAAI,CAAC;CACtD,CAAC;CACD,OAAO,OAAO;EACZ,MAAM,OAAO,QAAQ,QAAQ;EAC7B,YAAY,OAAO;EACnB,MAAM,OAAO,OAAO;GAAE,KAAK,OAAO;GAAQ,KAAK;EAAI,CAAC;CACtD,CAAC;AACH,CAAC;;;;;;AAOD,MAAa,cAAc,OAAO,OAAO;;CAEvC,WAAW,OAAO;CAClB,UAAU,OAAO;CACjB,OAAO,OAAO;CACd,MAAM,OAAO,YAAY,OAAO,MAAM,OAAO,MAAM,CAAC;CACpD,KAAK,OAAO,YAAY,OAAO,MAAM;;CAErC,YAAY,OAAO,MAAM,gBAAgB;;CAEzC,QAAQ,OAAO,YAAY,GAAG;;CAE9B,OAAO,OAAO,MAAM,eAAe;;CAEnC,UAAU,OAAO;CACjB,OAAO;AACT,CAAC;;AAID,MAAM,eAAe,OAAO,OAAO;CAAE,WAAW,OAAO;CAAQ,UAAU,OAAO;AAAO,CAAC;AACxF,MAAM,QAAQ,OAAO,oBAAoB,YAAY;AAErD,MAAM,iBAAiB;;AAGvB,MAAM,qBAAqB,OAAO,OAAO;CACvC,SAAS,OAAO,QAAQ,cAAc;CACtC,OAAO,OAAO;CACd,SAAS,OAAO,MAAM,OAAO,OAAO;CACpC,MAAM,OAAO,MAAM,OAAO,MAAM;AAClC,CAAC;AAQD,MAAa,oBAAkC;CAAE,SAAS,CAAC;CAAG,MAAM,CAAC;AAAE;;AAUvE,MAAa,YACX,MACA,YAC0B;CAC1B,IAAI,QAAQ,OAAM,cAAa,KAAK,SAAS,SAAS,CAAC,GAAG,OAAO;CACjE,MAAM,OAAO,IAAI,IAAI,IAAI;CACzB,KAAK,MAAM,aAAa,SAAS,KAAK,IAAI,SAAS;CACnD,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,IAAY;AACrC;;;;;;;AAQA,MAAa,eAAe,OAAqB,SAAqC;CACpF,MAAM,OAAO,SAAS,MAAM,MAAM,KAAK,IAAI;CAC3C,MAAM,OAAO,IAAI,IAAI,IAAI;CACzB,MAAM,uBAAO,IAAI,IAAkE;CACnF,KAAK,MAAM,OAAO,CAAC,GAAG,MAAM,SAAS,GAAG,KAAK,OAAO,GAAG;EACrD,MAAM,MAAM,MAAM,GAAG;EACrB,IAAI,OAAO,OAAO,GAAG,KAAK,KAAK,IAAI,IAAI,MAAM,SAAS,GAAG;EACzD,MAAM,OAAO,KAAK,IAAI,IAAI,MAAM,SAAS;EACzC,IAAI,SAAS,KAAA,KAAa,KAAK,WAAW,IAAI,MAAM,UAClD,KAAK,IAAI,IAAI,MAAM,WAAW;GAAE;GAAK,UAAU,IAAI,MAAM;EAAS,CAAC;CAEvE;CACA,OAAO;EAAE,SAAS,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG;EAAG;CAAK;AACnE;;AAGA,MAAa,cACX,KACA,UAEA,OAAO,IAAI,aAAa;CAEtB,MAAM,OAAO,QAAO,OADF,cAAc,cAAA,CACT,IAAI,GAAG;CAC9B,IAAI,SAAS,KAAA,GAAW,OAAO;CAC/B,MAAM,SAAS,OAAO,oBAAoB,OAAO,eAAe,kBAAkB,CAAC,CAAC,CAAC,IAAI;CACzF,OAAO,OAAO,MAAM,QAAQ;EAC1B,cAAc;EACd,SAAQ,WACN,OAAO,UAAU,QAAQ;GAAE,SAAS,OAAO;GAAS,MAAM,OAAO;EAAK,IAAI;CAC9E,CAAC;AACH,CAAC;;;;;;AAOH,MAAa,iBACX,KACA,UAEA,OAAO,IAAI,aAAa;CACtB,MAAM,OAAO,QAAQ,OAAO,cAAc,cAAA,CAAe,IAAI,GAAG;CAChE,OAAO,OAAO,OACZ,OAAO,QAAQ,OAAO,gBAAgB,IAAI,IAAG,WAC3C,OAAO,oBAAoB,OAAO,eAAe,kBAAkB,CAAC,CAAC,CAAC,MAAM,CAC9E,IACA,WAAU,OAAO,UAAU,SAAS,OAAO,QAAQ,SAAS,CAC9D;AACF,CAAC;AAEH,MAAa,cAAc,QAAsB,UAC/C,KAAK,UAAU;CAAE,SAAS;CAAgB;CAAO,SAAS,OAAO;CAAS,MAAM,OAAO;AAAK,CAAC;;AAG/F,MAAa,kBAAkB,SAC7B,OAAO,MAAM,OAAO,oBAAoB,OAAO,eAAe,kBAAkB,CAAC,CAAC,CAAC,IAAI,GAAG;CACxF,cAAc;CACd,SAAS,EAAE,SAAS,YAAY;EAAE;EAAS;CAAK;AAClD,CAAC;;;;;AAMH,MAAa,aAAa,QAAsB,cAC9C,OAAO,KAAK,SAAS,SAAS,KAC9B,OAAO,QAAQ,MAAK,QAClB,OAAO,OACL,OAAO,oBAAoB,WAAW,CAAC,CAAC,GAAG,IAC3C,UAAS,MAAM,cAAc,aAAa,MAAM,MAAM,SAAS,SACjE,CACF;AAwBF,MAAa,SAAS,KAAK,WAAmB;AAmB9C,MAAa,cAA2B;CACtC,SAAS,OAAO,KAAK;CACrB,KAAK,OAAO,KAAK;CACjB,OAAO,OAAO,KAAK;CACnB,OAAO,CAAC;CACR,MAAM,CAAC;AACT;AAEA,MAAa,cAAc,OAAO,OAAO;CACvC,WAAW,EAAE,YAAY,MAAM;CAC/B,aAAa,EAAE,gBAAgB;AACjC,CAAC;AAED,MAAa,aAAa,OAAO,OAAO;CACtC,WAAW,EAAE,YAAY,MAAM;CAC/B,aAAa,EAAE,eAAe;AAChC,CAAC;AAED,MAAM,WAAW,OAAO,OAAO;CAC7B,WAAW,EAAE,aAAa;CAC1B,aAAa,EAAE,aAAa;AAC9B,CAAC;;AAGD,MAAa,YAAY,YAAuC;CAC9D,SAAS,OAAO,MAAM,IACpB,OAAO,OAAO;EAAE,WAAW,EAAE,YAAY;EAAO,aAAa,EAAE,UAAU;CAAI,CAAC,CAChF;CACA,MAAM,OAAO;AACf;;AAGA,MAAa,aAAa,WAAiC,OAAO,MAAM,MAAM,QAAQ;;AAGtF,MAAa,UAAU,WAAoD;CACzE,KAAK,MAAM,UAAU,OAAO,OAC1B,IAAI,OAAO,SAAS,cAAc,OAAO,MAAM,MAAM,SAAS,aAAa,OAAO,QAChF,OAAO,OAAO,KAAK,OAAO,KAAK;CAGnC,OAAO,OAAO,KAAK;AACrB;;AAiBA,MAAa,eACX,QACA,YAIA,OAAO,QAAQ,SAAS,QAAkC;CACxD,MAAM,MAAM,MAAM,GAAG;CAErB,IAAI,OAAO,OAAO,GAAG,GAAG,OAAO,CAAC;CAChC,MAAM,cAAc,WAA6C,CAC/D;EAAE,MAAM;EAAc,GAAG,IAAI;EAAO;EAAK;CAAO,CAClD;CACA,MAAM,QAAQ,OAAO,oBAAoB,WAAW,CAAC,CAAC,GAAG;CACzD,IAAI,OAAO,OAAO,KAAK,GAAG,OAAO,WAAW,mDAAmD;CAC/F,OAAO,OAAO,MAAM,QAAQ,MAAM,MAAM,QAAQ,GAAG;EACjD,cAAc,WAAW,GAAG,MAAM,MAAM,SAAS,0CAA0C;EAC3F,SAAQ,WACN,OAAO,MAAM,OAAO,MAAM,MAAM,KAAK,GAAG;GACtC,cAAc,WAAW,kCAAkC,MAAM,MAAM,SAAS,GAAG;GACnF,SAAS,EAAE,cAAc,CAAC;IAAE,MAAM;IAAY,OAAO,MAAM;IAAO,OAAO;GAAQ,CAAC;EACpF,CAAC;CACL,CAAC;AACH,CAAC;AAkBH,MAAa,kBAAkB,KAAK,WAA4B;AAUhE,MAAM,eAAe,OAAO,OAAO,EAAE,UAAU,OAAO,OAAO,CAAC;AAE9D,MAAa,gBAAgB,WAAsC;CACjE,MAAM,QAAQ,OAAO,MAAM,IACzB,OAAO,OAAO;EACZ,WAAW,EAAE,OAAO,SAAS,eAA2B;GACtD,WAAW,MAAM;GACjB,UAAU,OAAO,KAAK,MAAM,QAAQ;GACpC,OACE,MAAM,MAAM,SAAS,YACjB,gBAAgB,QAAQ;IAAE;IAAS,OAAO;GAAQ,CAAC,IACnD,gBAAgB,OAAO,EAAE,OAAO,MAAM,MAAM,MAAM,CAAC;EAC3D;EACA,aAAa,EAAE,WAAW,KAAK,cAA0B;GACvD,WAAW;GACX,UAAU,OAAO,IACf,OAAO,oBAAoB,YAAY,CAAC,CAAC,GAAG,IAC3C,EAAE,eAAe,QACpB;GACA,OAAO,gBAAgB,WAAW,EAAE,OAAO,CAAC;EAC9C;CACF,CAAC,CACH;CACA,OAAO;EACL,SAAS,MAAM,QAAO,SAAQ,KAAK,MAAM,SAAS,SAAS,CAAC,CAAC;EAC7D,QAAQ,MAAM,QAAO,SAAQ,KAAK,MAAM,SAAS,QAAQ,CAAC,CAAC;EAC3D;CACF;AACF;;;;;AAMA,MAAa,cAAc,QAAQ,UACjC,8BACA,EAAE,oBAAoB,OAAO,gBAAgB,WAAW,WAAW,KAAK,EAAE,CAC5E;;;;;AAMA,MAAa,YACX,MACA,WAEA,OAAO,QAAQ,OAAO,QAAQ,WAAW,IAAG,SAC1C,OAAO,MAAM,MAAM;CACjB,cAAc;CACd,SAAQ,UAAS,UAAU,OAAO,MAAM,MAAM;AAChD,CAAC,CACH;AAEF,MAAM,aACJ,OACA,MACA,WAEA,OAAO,kBACL,OAAO,UAAsB,QAAQ,WAAW;CAC9C,MACG,QAAQ,MAAM,EAAE,OAAO,SAAS;EAC/B,IAAI,gBAA4B,KAAA;EAChC,MAAM,OAAO,IAAI,SAAc,SAAQ;GACrC,UAAU;EACZ,CAAC;EACD,OAAO,OAAO,QAAQ,OAAO,CAAC;EAC9B,OAAO;CACT,CAAC,CAAC,CAED,YAAY,KAAA,CAAS;AAC1B,CAAC,SACK,SACN,YAAW,OAAO,KAAK,OAAO,CAChC;;AAGF,MAAa,cAAc,QACzB,OAAO,WAAW,qBAAqB,aACnC,OAAO,KAAK,IAAI,WAAW,iBAAiB,yBAAyB,KAAK,CAAC,IAC3E,OAAO,KAAK;;AAGlB,MAAa,YAAY,QAAwB,yBAAyB,IAAI;;AAG9E,MAAa,aAAa,QAAwB,yBAAyB,IAAI;;AAW/E,MAAa,WACX,SACA,UAAwB,CAAC,MACY;CACrC,MAAM,OAAO,SAAS,SAAS,SAAS,gBAAgB,QAAQ,QAAQ,UAAU,CAAC;CACnF,MAAM,MAAM,SAAS,SAAS,SAAS,gBAAgB,QAAQ,OAAO,YAAY,CAAC;CACnF,MAAM,OAAO,KAAK,IAAI,KAAK,OAAO,KAAK,KAAK,IAAI,GAAG,UAAU,CAAC,CAAC;CAC/D,OAAO,OAAO,IAAI,OAAO,YAAY,IAAK,GAAG,IAAG,WAAU,SAAS,OAAO,OAAO,MAAM,CAAC;AAC1F;;;;;;AAOA,MAAa,cAAmC,OAAO,UAAe,WAAU;CAC9E,MAAM,SAAS,WAAW;CAC1B,IAAI,WAAW,KAAA,KAAa,OAAO,OAAO,qBAAqB,YAAY;CAC3E,MAAM,eAAe,OAAO,OAAO,IAAI;CACvC,OAAO,iBAAiB,UAAU,QAAQ,EAAE,MAAM,KAAK,CAAC;CACxD,OAAO,OAAO,WAAW,OAAO,oBAAoB,UAAU,MAAM,CAAC;AACvE,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { r as EntityDescriptor } from "./entity-CgsG5Uel.js";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
/**
|
|
4
|
+
* `Remote.patch`'s values as the store holds them: each relation's refs
|
|
5
|
+
* written as ref keys. A relation given anything but a ref to its target (a
|
|
6
|
+
* ref key typed by hand, a ref to another Entity) is refused here, where the
|
|
7
|
+
* patch is made.
|
|
8
|
+
*/
|
|
9
|
+
export declare const wireValues: (descriptor: EntityDescriptor<string, Schema.Struct.Fields>, values: Readonly<Record<string, unknown>>) => Readonly<Record<string, unknown>>;
|
|
10
|
+
//# sourceMappingURL=patchValues.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patchValues.d.ts","names":[],"sources":["../src/patchValues.ts"],"mappings":";;;;;;;;qBAoDa,aAAU,YACT,yBAAyB,OAAO,OAAO,SAAO,QAClD,SAAS,6BAChB,SAAS"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { relationShape } from "./relation.js";
|
|
2
|
+
import { entityKey } from "./store.js";
|
|
3
|
+
// src/patchValues.ts
|
|
4
|
+
const relationShapes = /* @__PURE__ */ new WeakMap();
|
|
5
|
+
/** A descriptor's relation fields and their shapes, read once per descriptor. */
|
|
6
|
+
const relationsOf = (fields) => {
|
|
7
|
+
const known = relationShapes.get(fields);
|
|
8
|
+
if (known !== void 0) return known;
|
|
9
|
+
const shapes = /* @__PURE__ */ new Map();
|
|
10
|
+
for (const [key, schema] of Object.entries(fields)) {
|
|
11
|
+
const shape = relationShape(schema);
|
|
12
|
+
if (shape !== void 0) shapes.set(key, shape);
|
|
13
|
+
}
|
|
14
|
+
relationShapes.set(fields, shapes);
|
|
15
|
+
return shapes;
|
|
16
|
+
};
|
|
17
|
+
const isRefTo = (value, entity) => value !== null && typeof value === "object" && value.entity === entity && typeof value.id === "string";
|
|
18
|
+
/** One relation value as the store holds it, or a refusal naming what the field takes. */
|
|
19
|
+
const wireRelation = (shape, value, refuse) => {
|
|
20
|
+
if (value === void 0) return value;
|
|
21
|
+
if (value === null) return shape.nullable ? null : refuse();
|
|
22
|
+
const key = (ref) => isRefTo(ref, shape.entity) ? entityKey(ref.entity, ref.id) : refuse();
|
|
23
|
+
if (shape.kind === "one") return key(value);
|
|
24
|
+
if (shape.kind === "many") return Array.isArray(value) ? value.map(key) : refuse();
|
|
25
|
+
const page = value;
|
|
26
|
+
return Array.isArray(page.refs) && typeof page.hasNext === "boolean" && typeof page.hasPrevious === "boolean" ? {
|
|
27
|
+
refs: page.refs.map(key),
|
|
28
|
+
hasNext: page.hasNext,
|
|
29
|
+
hasPrevious: page.hasPrevious
|
|
30
|
+
} : refuse();
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* `Remote.patch`'s values as the store holds them: each relation's refs
|
|
34
|
+
* written as ref keys. A relation given anything but a ref to its target (a
|
|
35
|
+
* ref key typed by hand, a ref to another Entity) is refused here, where the
|
|
36
|
+
* patch is made.
|
|
37
|
+
*/
|
|
38
|
+
const wireValues = (descriptor, values) => {
|
|
39
|
+
const relations = relationsOf(descriptor.fields);
|
|
40
|
+
if (relations.size === 0) return values;
|
|
41
|
+
return Object.fromEntries(Object.entries(values).map(([field, value]) => {
|
|
42
|
+
const shape = relations.get(field);
|
|
43
|
+
if (shape === void 0) return [field, value];
|
|
44
|
+
const refuse = () => {
|
|
45
|
+
const takes = {
|
|
46
|
+
one: "a ref",
|
|
47
|
+
many: "an array of refs",
|
|
48
|
+
page: "a page of refs"
|
|
49
|
+
}[shape.kind];
|
|
50
|
+
throw new Error(`Remote.patch: "${field}" of "${descriptor.name}" takes ${takes} to ${shape.entity} (Remote.ref(${shape.entity}, id))${shape.nullable ? " or null" : ""}, not ${JSON.stringify(value)}`);
|
|
51
|
+
};
|
|
52
|
+
return [field, wireRelation(shape, value, refuse)];
|
|
53
|
+
}));
|
|
54
|
+
};
|
|
55
|
+
export { wireValues };
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=patchValues.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patchValues.js","names":[],"sources":["../src/patchValues.ts"],"sourcesContent":["/**\n * `Remote.patch`'s relation values written as the store holds them: a ref\n * becomes its ref key, and anything else in a relation is refused.\n */\nimport type { Schema } from 'effect'\nimport type { EntityDescriptor, EntityRef, RefPage } from './entity.js'\nimport { relationShape, type RelationShape } from './relation.js'\nimport { entityKey } from './store.js'\n\nconst relationShapes = new WeakMap<Schema.Struct.Fields, ReadonlyMap<string, RelationShape>>()\n\n/** A descriptor's relation fields and their shapes, read once per descriptor. */\nconst relationsOf = (fields: Schema.Struct.Fields): ReadonlyMap<string, RelationShape> => {\n const known = relationShapes.get(fields)\n if (known !== undefined) return known\n const shapes = new Map<string, RelationShape>()\n for (const [key, schema] of Object.entries(fields)) {\n const shape = relationShape(schema)\n if (shape !== undefined) shapes.set(key, shape)\n }\n relationShapes.set(fields, shapes)\n return shapes\n}\n\nconst isRefTo = (value: unknown, entity: string): value is EntityRef<string> =>\n value !== null &&\n typeof value === 'object' &&\n (value as Partial<EntityRef<string>>).entity === entity &&\n typeof (value as Partial<EntityRef<string>>).id === 'string'\n\n/** One relation value as the store holds it, or a refusal naming what the field takes. */\nconst wireRelation = (shape: RelationShape, value: unknown, refuse: () => never): unknown => {\n if (value === undefined) return value\n if (value === null) return shape.nullable ? null : refuse()\n const key = (ref: unknown): string =>\n isRefTo(ref, shape.entity) ? entityKey(ref.entity, ref.id) : refuse()\n if (shape.kind === 'one') return key(value)\n if (shape.kind === 'many') return Array.isArray(value) ? value.map(key) : refuse()\n const page = value as Partial<RefPage<string>>\n return Array.isArray(page.refs) &&\n typeof page.hasNext === 'boolean' &&\n typeof page.hasPrevious === 'boolean'\n ? { refs: page.refs.map(key), hasNext: page.hasNext, hasPrevious: page.hasPrevious }\n : refuse()\n}\n\n/**\n * `Remote.patch`'s values as the store holds them: each relation's refs\n * written as ref keys. A relation given anything but a ref to its target (a\n * ref key typed by hand, a ref to another Entity) is refused here, where the\n * patch is made.\n */\nexport const wireValues = (\n descriptor: EntityDescriptor<string, Schema.Struct.Fields>,\n values: Readonly<Record<string, unknown>>,\n): Readonly<Record<string, unknown>> => {\n const relations = relationsOf(descriptor.fields)\n if (relations.size === 0) return values\n return Object.fromEntries(\n Object.entries(values).map(([field, value]) => {\n const shape = relations.get(field)\n if (shape === undefined) return [field, value]\n const refuse = (): never => {\n const takes = { one: 'a ref', many: 'an array of refs', page: 'a page of refs' }[shape.kind]\n throw new Error(\n `Remote.patch: \"${field}\" of \"${descriptor.name}\" takes ${takes} to ${shape.entity} (Remote.ref(${shape.entity}, id))${shape.nullable ? ' or null' : ''}, not ${JSON.stringify(value)}`,\n )\n }\n return [field, wireRelation(shape, value, refuse)]\n }),\n )\n}\n"],"mappings":";;;AASA,MAAM,iCAAiB,IAAI,QAAkE;;AAG7F,MAAM,eAAe,WAAqE;CACxF,MAAM,QAAQ,eAAe,IAAI,MAAM;CACvC,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,MAAM,yBAAS,IAAI,IAA2B;CAC9C,KAAK,MAAM,CAAC,KAAK,WAAW,OAAO,QAAQ,MAAM,GAAG;EAClD,MAAM,QAAQ,cAAc,MAAM;EAClC,IAAI,UAAU,KAAA,GAAW,OAAO,IAAI,KAAK,KAAK;CAChD;CACA,eAAe,IAAI,QAAQ,MAAM;CACjC,OAAO;AACT;AAEA,MAAM,WAAW,OAAgB,WAC/B,UAAU,QACV,OAAO,UAAU,YAChB,MAAqC,WAAW,UACjD,OAAQ,MAAqC,OAAO;;AAGtD,MAAM,gBAAgB,OAAsB,OAAgB,WAAiC;CAC3F,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,IAAI,UAAU,MAAM,OAAO,MAAM,WAAW,OAAO,OAAO;CAC1D,MAAM,OAAO,QACX,QAAQ,KAAK,MAAM,MAAM,IAAI,UAAU,IAAI,QAAQ,IAAI,EAAE,IAAI,OAAO;CACtE,IAAI,MAAM,SAAS,OAAO,OAAO,IAAI,KAAK;CAC1C,IAAI,MAAM,SAAS,QAAQ,OAAO,MAAM,QAAQ,KAAK,IAAI,MAAM,IAAI,GAAG,IAAI,OAAO;CACjF,MAAM,OAAO;CACb,OAAO,MAAM,QAAQ,KAAK,IAAI,KAC5B,OAAO,KAAK,YAAY,aACxB,OAAO,KAAK,gBAAgB,YAC1B;EAAE,MAAM,KAAK,KAAK,IAAI,GAAG;EAAG,SAAS,KAAK;EAAS,aAAa,KAAK;CAAY,IACjF,OAAO;AACb;;;;;;;AAQA,MAAa,cACX,YACA,WACsC;CACtC,MAAM,YAAY,YAAY,WAAW,MAAM;CAC/C,IAAI,UAAU,SAAS,GAAG,OAAO;CACjC,OAAO,OAAO,YACZ,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW;EAC7C,MAAM,QAAQ,UAAU,IAAI,KAAK;EACjC,IAAI,UAAU,KAAA,GAAW,OAAO,CAAC,OAAO,KAAK;EAC7C,MAAM,eAAsB;GAC1B,MAAM,QAAQ;IAAE,KAAK;IAAS,MAAM;IAAoB,MAAM;GAAiB,EAAE,MAAM;GACvF,MAAM,IAAI,MACR,kBAAkB,MAAM,QAAQ,WAAW,KAAK,UAAU,MAAM,MAAM,MAAM,OAAO,eAAe,MAAM,OAAO,QAAQ,MAAM,WAAW,aAAa,GAAG,QAAQ,KAAK,UAAU,KAAK,GACtL;EACF;EACA,OAAO,CAAC,OAAO,aAAa,OAAO,OAAO,MAAM,CAAC;CACnD,CAAC,CACH;AACF"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { i as Edge, n as Connection } from "./connection-BRyaJy89.js";
|
|
2
|
+
import { r as EntityStore } from "./store-ah2qjcJW.js";
|
|
3
|
+
import { n as ConnectionIdentity } from "./optimistic-CECsfNGY.js";
|
|
4
|
+
import { Effect, Option } from "effect";
|
|
5
|
+
import { KeyValueStore } from "effect/persistence";
|
|
6
|
+
/** Bump when the serialized shape changes; a mismatch discards the cache. */
|
|
7
|
+
declare const REMOTE_CACHE_VERSION = 5;
|
|
8
|
+
/**
|
|
9
|
+
* What survives a reload: the entity cache, and the connections an application
|
|
10
|
+
* **declared** should come back.
|
|
11
|
+
*
|
|
12
|
+
* Connections are session state by default and stay that way — `snapshotOf`
|
|
13
|
+
* takes none unless asked. What a declared one keeps is its **edges and
|
|
14
|
+
* nothing else**, which is not a simplification but the rule: a connection's
|
|
15
|
+
* boundaries are cursors the server minted, and a cursor may name server state
|
|
16
|
+
* that no longer exists. Restoring one would be claiming to know where the
|
|
17
|
+
* page ends. The shape has nowhere to put a cursor, so that cannot be got
|
|
18
|
+
* wrong later.
|
|
19
|
+
*/
|
|
20
|
+
interface Snapshot {
|
|
21
|
+
readonly entities: EntityStore;
|
|
22
|
+
/**
|
|
23
|
+
* By connection identity: its **segments**, each a run of edges. No
|
|
24
|
+
* boundaries.
|
|
25
|
+
*
|
|
26
|
+
* Segments rather than one flat list, because the gaps between them are
|
|
27
|
+
* knowledge too. A connection paged from both ends holds a head and a tail
|
|
28
|
+
* with an unfetched middle; flattening those into one run would claim the
|
|
29
|
+
* tail's first row follows the head's last, which is a thing the client was
|
|
30
|
+
* never told.
|
|
31
|
+
*/
|
|
32
|
+
readonly connections: Readonly<Record<string, ReadonlyArray<ReadonlyArray<Edge>>>>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A snapshot of a Model, keeping the entity cache and only the connections
|
|
36
|
+
* named.
|
|
37
|
+
*
|
|
38
|
+
* Naming none — the default — is exactly what Remote did before connections
|
|
39
|
+
* could be kept at all: a disposable, server-derived cache.
|
|
40
|
+
*/
|
|
41
|
+
declare const snapshotOf: (model: {
|
|
42
|
+
readonly entities: EntityStore;
|
|
43
|
+
readonly connections: Readonly<Record<string, Connection>>;
|
|
44
|
+
}, options?: {
|
|
45
|
+
readonly connections?: ReadonlyArray<ConnectionIdentity> | undefined;
|
|
46
|
+
}) => Snapshot;
|
|
47
|
+
interface SnapshotOptions {
|
|
48
|
+
/**
|
|
49
|
+
* Who or what the snapshot is for (a user id, a tenant, a build). A snapshot
|
|
50
|
+
* taken under another scope is discarded rather than shown to the wrong
|
|
51
|
+
* reader. Default: unscoped, which only matches unscoped.
|
|
52
|
+
*/
|
|
53
|
+
readonly scope?: string | undefined;
|
|
54
|
+
/** Snapshots larger than this many bytes are not written and not read. */
|
|
55
|
+
readonly maxBytes?: number | undefined;
|
|
56
|
+
}
|
|
57
|
+
/** How a snapshot meets a store that already has entries. */
|
|
58
|
+
type MergePolicy = 'replace' | 'preserve-existing';
|
|
59
|
+
/**
|
|
60
|
+
* The snapshot text, or `undefined` when it would exceed `maxBytes`. Build the
|
|
61
|
+
* snapshot with `snapshotOf`, which is where the connections that survive are
|
|
62
|
+
* named; runtime state — optimistic layers, live cursors, gaps, the mutation
|
|
63
|
+
* ledger — is never in one.
|
|
64
|
+
*/
|
|
65
|
+
declare function dehydrate(snapshot: Snapshot, options?: SnapshotOptions & {
|
|
66
|
+
readonly maxBytes?: undefined;
|
|
67
|
+
}): string;
|
|
68
|
+
declare function dehydrate(snapshot: Snapshot, options?: SnapshotOptions): string | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* The store a snapshot text holds, or none when the text is missing,
|
|
71
|
+
* oversized, not this version, for another scope, or malformed. Hydrating the
|
|
72
|
+
* same text twice yields equal stores.
|
|
73
|
+
*/
|
|
74
|
+
declare const hydrate: (raw: string | undefined | null, options?: SnapshotOptions) => Option.Option<Snapshot>;
|
|
75
|
+
/**
|
|
76
|
+
* Brings a snapshot into a store that may already hold entries. `replace`
|
|
77
|
+
* takes the snapshot's entry for every key it has; `preserve-existing` keeps
|
|
78
|
+
* an entry the current store already has (it is at least as fresh as a
|
|
79
|
+
* snapshot taken earlier). Either way, keys only the current store has stay.
|
|
80
|
+
*/
|
|
81
|
+
declare const mergeStores: (current: EntityStore, snapshot: EntityStore, policy: MergePolicy) => EntityStore;
|
|
82
|
+
declare const RemotePersistence: {
|
|
83
|
+
/** The snapshot text, or `undefined` when it would exceed `maxBytes`. */
|
|
84
|
+
dehydrate: typeof dehydrate;
|
|
85
|
+
/** What a snapshot text holds, or none when it is refused. */
|
|
86
|
+
hydrate: typeof hydrate;
|
|
87
|
+
/**
|
|
88
|
+
* What to keep of a Model, naming the connections that should survive a
|
|
89
|
+
* reload. Naming none is the default and is what Remote always did.
|
|
90
|
+
*/
|
|
91
|
+
snapshotOf: typeof snapshotOf;
|
|
92
|
+
/** A snapshot's entities brought into a store by policy. */
|
|
93
|
+
mergeStores: typeof mergeStores;
|
|
94
|
+
/** The cache as a fresh session has it; what a refused `restore` yields. */
|
|
95
|
+
emptySnapshot: Snapshot;
|
|
96
|
+
/**
|
|
97
|
+
* Writes the snapshot under `key`, or removes the key when the store would
|
|
98
|
+
* exceed `maxBytes`, so a stale smaller snapshot cannot outlive a larger
|
|
99
|
+
* store it no longer describes.
|
|
100
|
+
*/
|
|
101
|
+
save: (snapshot: Snapshot, options: {
|
|
102
|
+
readonly key: string;
|
|
103
|
+
} & SnapshotOptions) => Effect.Effect<void, KeyValueStore.KeyValueStoreError, KeyValueStore.KeyValueStore>;
|
|
104
|
+
/**
|
|
105
|
+
* Reads the cache. A missing key yields `emptySnapshot`; anything `hydrate`
|
|
106
|
+
* refuses yields `emptySnapshot` and removes the key, so the next `restore`
|
|
107
|
+
* is clean and the planner refetches.
|
|
108
|
+
*/
|
|
109
|
+
restore: (options: {
|
|
110
|
+
readonly key: string;
|
|
111
|
+
} & SnapshotOptions) => Effect.Effect<Snapshot, KeyValueStore.KeyValueStoreError, KeyValueStore.KeyValueStore>;
|
|
112
|
+
};
|
|
113
|
+
export { SnapshotOptions as a, Snapshot as i, REMOTE_CACHE_VERSION as n, RemotePersistence as r, MergePolicy as t };
|
|
114
|
+
//# sourceMappingURL=persistence-CCMnvg7-.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence-CCMnvg7-.d.ts","names":[],"sources":["../src/persistence.ts"],"mappings":";;;;;;cAqBa;;;;;;;;;;;;;UAcI;WACN,UAAU;;;;;;;;;;;WAWV,aAAa,SAAS,eAAe,cAAc,cAAc;;;;;;;;;cAUtE,aAAU;WAEH,UAAU;WACV,aAAa,SAAS,eAAe;GAC/C;WACmB,cAAc,cAAc;MAC/C;UAac;;;;;;WAMN;;WAEA;;;KAIC;;;;;;;iBAiHH,UACP,UAAU,UACV,UAAU;WAA6B;;iBAEhC,UAAU,UAAU,UAAU,UAAU;;;;;;cAuB3C,UAAO,gCACmB,UACrB,oBACR,OAAO,OAAO;;;;;;;cAgCX,cAAW,SACN,aAAW,UACV,aAAW,QACb,gBACP;cAUU;;;;;;;;;;;;;;;;;;;EAoBM,OAAA,UAAA,UAAQ;aAAsB;MAAgB,oBAAe,OAAA,aAAA,cAAA,oBAAA,cAAA;;;;;;EAa3D,UAAA;aAAW;MAAgB,oBAAe,OAAA,OAAA,UAAA,cAAA,oBAAA,cAAA"}
|