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.
Files changed (198) hide show
  1. package/README.md +987 -198
  2. package/dist/answerGaps.d.ts +16 -0
  3. package/dist/answerGaps.d.ts.map +1 -0
  4. package/dist/answerGaps.js +60 -0
  5. package/dist/answerGaps.js.map +1 -0
  6. package/dist/client-B055kQuP.d.ts +110 -0
  7. package/dist/client-B055kQuP.d.ts.map +1 -0
  8. package/dist/client.d.ts +2 -0
  9. package/dist/client.js +142 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/coalesce-CLAr92Nk.d.ts +34 -0
  12. package/dist/coalesce-CLAr92Nk.d.ts.map +1 -0
  13. package/dist/coalesce.d.ts +2 -0
  14. package/dist/coalesce.js +131 -0
  15. package/dist/coalesce.js.map +1 -0
  16. package/dist/connection-BRyaJy89.d.ts +45 -0
  17. package/dist/connection-BRyaJy89.d.ts.map +1 -0
  18. package/dist/connection.d.ts +2 -0
  19. package/dist/connection.js +170 -0
  20. package/dist/connection.js.map +1 -0
  21. package/dist/data.d.ts +7 -0
  22. package/dist/data.d.ts.map +1 -0
  23. package/dist/data.js +21 -0
  24. package/dist/data.js.map +1 -0
  25. package/dist/diagnostics-DHhVYflV.d.ts +125 -0
  26. package/dist/diagnostics-DHhVYflV.d.ts.map +1 -0
  27. package/dist/diagnostics.d.ts +2 -0
  28. package/dist/diagnostics.js +60 -0
  29. package/dist/diagnostics.js.map +1 -0
  30. package/dist/entity-CgsG5Uel.d.ts +196 -0
  31. package/dist/entity-CgsG5Uel.d.ts.map +1 -0
  32. package/dist/entity-DuDxaa3n.js +279 -0
  33. package/dist/entity-DuDxaa3n.js.map +1 -0
  34. package/dist/entity.d.ts +2 -0
  35. package/dist/entity.js +1 -0
  36. package/dist/held.d.ts +19 -0
  37. package/dist/held.d.ts.map +1 -0
  38. package/dist/held.js +70 -0
  39. package/dist/held.js.map +1 -0
  40. package/dist/impact.d.ts +79 -0
  41. package/dist/impact.d.ts.map +1 -0
  42. package/dist/impact.js +166 -0
  43. package/dist/impact.js.map +1 -0
  44. package/dist/index.d.ts +1542 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +2791 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/inspect-DtRQGCRP.d.ts +134 -0
  49. package/dist/inspect-DtRQGCRP.d.ts.map +1 -0
  50. package/dist/inspect.d.ts +2 -0
  51. package/dist/inspect.js +33 -0
  52. package/dist/inspect.js.map +1 -0
  53. package/dist/json-DowLl1eB.d.ts +52 -0
  54. package/dist/json-DowLl1eB.d.ts.map +1 -0
  55. package/dist/json.d.ts +2 -0
  56. package/dist/json.js +123 -0
  57. package/dist/json.js.map +1 -0
  58. package/dist/law.d.ts +56 -0
  59. package/dist/law.d.ts.map +1 -0
  60. package/dist/law.js +201 -0
  61. package/dist/law.js.map +1 -0
  62. package/dist/live-BpTzlCnY.d.ts +79 -0
  63. package/dist/live-BpTzlCnY.d.ts.map +1 -0
  64. package/dist/live.d.ts +2 -0
  65. package/dist/live.js +126 -0
  66. package/dist/live.js.map +1 -0
  67. package/dist/matching-DF5F9Us8.d.ts +100 -0
  68. package/dist/matching-DF5F9Us8.d.ts.map +1 -0
  69. package/dist/matching.d.ts +2 -0
  70. package/dist/matching.js +172 -0
  71. package/dist/matching.js.map +1 -0
  72. package/dist/model-B6zk1NMB.d.ts +710 -0
  73. package/dist/model-B6zk1NMB.d.ts.map +1 -0
  74. package/dist/model.d.ts +2 -0
  75. package/dist/model.js +1308 -0
  76. package/dist/model.js.map +1 -0
  77. package/dist/mutation.d.ts +2 -0
  78. package/dist/mutation.js +612 -0
  79. package/dist/mutation.js.map +1 -0
  80. package/dist/optimistic-CECsfNGY.d.ts +523 -0
  81. package/dist/optimistic-CECsfNGY.d.ts.map +1 -0
  82. package/dist/optimistic.d.ts +2 -0
  83. package/dist/optimistic.js +336 -0
  84. package/dist/optimistic.js.map +1 -0
  85. package/dist/outbox-DNuc4kZp.d.ts +259 -0
  86. package/dist/outbox-DNuc4kZp.d.ts.map +1 -0
  87. package/dist/outbox.d.ts +2 -0
  88. package/dist/outbox.js +326 -0
  89. package/dist/outbox.js.map +1 -0
  90. package/dist/patchValues.d.ts +10 -0
  91. package/dist/patchValues.d.ts.map +1 -0
  92. package/dist/patchValues.js +57 -0
  93. package/dist/patchValues.js.map +1 -0
  94. package/dist/persistence-CCMnvg7-.d.ts +114 -0
  95. package/dist/persistence-CCMnvg7-.d.ts.map +1 -0
  96. package/dist/persistence.d.ts +2 -0
  97. package/dist/persistence.js +205 -0
  98. package/dist/persistence.js.map +1 -0
  99. package/dist/placement-jZdsIpmj.d.ts +50 -0
  100. package/dist/placement-jZdsIpmj.d.ts.map +1 -0
  101. package/dist/placement.d.ts +2 -0
  102. package/dist/placement.js +116 -0
  103. package/dist/placement.js.map +1 -0
  104. package/dist/plan-D9cftxC_.d.ts +35 -0
  105. package/dist/plan-D9cftxC_.d.ts.map +1 -0
  106. package/dist/plan.d.ts +2 -0
  107. package/dist/plan.js +100 -0
  108. package/dist/plan.js.map +1 -0
  109. package/dist/policy-DsWHmIAq.d.ts +42 -0
  110. package/dist/policy-DsWHmIAq.d.ts.map +1 -0
  111. package/dist/policy.d.ts +2 -0
  112. package/dist/policy.js +32 -0
  113. package/dist/policy.js.map +1 -0
  114. package/dist/{port.d.mts → port.d.ts} +3 -2
  115. package/dist/port.d.ts.map +1 -0
  116. package/dist/{port.mjs → port.js} +3 -2
  117. package/dist/port.js.map +1 -0
  118. package/dist/query-BuIU93xA.d.ts +141 -0
  119. package/dist/query-BuIU93xA.d.ts.map +1 -0
  120. package/dist/query.d.ts +2 -0
  121. package/dist/query.js +0 -0
  122. package/dist/query.js.map +1 -0
  123. package/dist/record.d.ts +11 -0
  124. package/dist/record.d.ts.map +1 -0
  125. package/dist/record.js +21 -0
  126. package/dist/record.js.map +1 -0
  127. package/dist/relation-BLBGOZom.d.ts +59 -0
  128. package/dist/relation-BLBGOZom.d.ts.map +1 -0
  129. package/dist/relation.d.ts +2 -0
  130. package/dist/relation.js +93 -0
  131. package/dist/relation.js.map +1 -0
  132. package/dist/remoteData-mK1suYi1.d.ts +148 -0
  133. package/dist/remoteData-mK1suYi1.d.ts.map +1 -0
  134. package/dist/remoteData.d.ts +2 -0
  135. package/dist/remoteData.js +179 -0
  136. package/dist/remoteData.js.map +1 -0
  137. package/dist/reporting.d.ts +12 -0
  138. package/dist/reporting.d.ts.map +1 -0
  139. package/dist/reporting.js +45 -0
  140. package/dist/reporting.js.map +1 -0
  141. package/dist/requirement-D57S2LpH.d.ts +64 -0
  142. package/dist/requirement-D57S2LpH.d.ts.map +1 -0
  143. package/dist/requirement.d.ts +2 -0
  144. package/dist/requirement.js +114 -0
  145. package/dist/requirement.js.map +1 -0
  146. package/dist/resume-Ct5R6yli.d.ts +92 -0
  147. package/dist/resume-Ct5R6yli.d.ts.map +1 -0
  148. package/dist/resume.d.ts +2 -0
  149. package/dist/resume.js +222 -0
  150. package/dist/resume.js.map +1 -0
  151. package/dist/retain-BfNTvBRb.d.ts +48 -0
  152. package/dist/retain-BfNTvBRb.d.ts.map +1 -0
  153. package/dist/retain.d.ts +2 -0
  154. package/dist/retain.js +107 -0
  155. package/dist/retain.js.map +1 -0
  156. package/dist/selection.d.ts +2 -0
  157. package/dist/selection.js +1 -0
  158. package/dist/shown.d.ts +20 -0
  159. package/dist/shown.d.ts.map +1 -0
  160. package/dist/shown.js +40 -0
  161. package/dist/shown.js.map +1 -0
  162. package/dist/sse-C7LhdbiQ.d.ts +17 -0
  163. package/dist/sse-C7LhdbiQ.d.ts.map +1 -0
  164. package/dist/sse.d.ts +2 -0
  165. package/dist/sse.js +127 -0
  166. package/dist/sse.js.map +1 -0
  167. package/dist/store-ah2qjcJW.d.ts +100 -0
  168. package/dist/store-ah2qjcJW.d.ts.map +1 -0
  169. package/dist/store.d.ts +2 -0
  170. package/dist/store.js +218 -0
  171. package/dist/store.js.map +1 -0
  172. package/dist/testing.d.ts +133 -0
  173. package/dist/testing.d.ts.map +1 -0
  174. package/dist/testing.js +0 -0
  175. package/dist/testing.js.map +1 -0
  176. package/dist/why-kgp_Wr3z.d.ts +483 -0
  177. package/dist/why-kgp_Wr3z.d.ts.map +1 -0
  178. package/dist/why.d.ts +2 -0
  179. package/dist/why.js +293 -0
  180. package/dist/why.js.map +1 -0
  181. package/dist/whyReasons.d.ts +42 -0
  182. package/dist/whyReasons.d.ts.map +1 -0
  183. package/dist/whyReasons.js +211 -0
  184. package/dist/whyReasons.js.map +1 -0
  185. package/dist/wire-DthPcTM6.d.ts +787 -0
  186. package/dist/wire-DthPcTM6.d.ts.map +1 -0
  187. package/dist/wire.d.ts +2 -0
  188. package/dist/wire.js +329 -0
  189. package/dist/wire.js.map +1 -0
  190. package/package.json +19 -14
  191. package/dist/index-BEvVAL76.d.mts +0 -4097
  192. package/dist/index-BEvVAL76.d.mts.map +0 -1
  193. package/dist/index.d.mts +0 -2
  194. package/dist/index.mjs +0 -1
  195. package/dist/port.d.mts.map +0 -1
  196. package/dist/port.mjs.map +0 -1
  197. package/dist/src-BDqn7Ocr.mjs +0 -6344
  198. package/dist/src-BDqn7Ocr.mjs.map +0 -1
@@ -0,0 +1,17 @@
1
+ import { n as RemoteJsonClient, r as RemoteRpcClient } from "./client-B055kQuP.js";
2
+ /** What `httpWithLive` needs besides the URL. Mirrors `http`, plus the fetch itself. */
3
+ interface LiveFetchOptions {
4
+ /** Read per request, so a token that changes reaches the next call. */
5
+ readonly headers?: (() => Readonly<Record<string, string>>) | undefined;
6
+ /** Defaults to the global one; pass it to route calls elsewhere in tests. */
7
+ readonly fetch?: typeof fetch | undefined;
8
+ }
9
+ /** Remote's live stream over fetch: `serveFetch`'s live open as a `Stream`. */
10
+ declare const liveFetch: (url: string, options?: LiveFetchOptions) => RemoteRpcClient['FoldkitRemoteLive'];
11
+ /**
12
+ * Remote's client over HTTP with live: reads, queries, and mutations as
13
+ * `http` sends them, live as `liveFetch` streams it, for `clientLayer`.
14
+ */
15
+ declare const httpWithLive: (url: string, options?: LiveFetchOptions) => RemoteJsonClient;
16
+ export { httpWithLive as n, liveFetch as r, LiveFetchOptions as t };
17
+ //# sourceMappingURL=sse-C7LhdbiQ.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse-C7LhdbiQ.d.ts","names":[],"sources":["../src/sse.ts"],"mappings":";;UAuBiB;;WAEN,iBAAiB,SAAS;;WAE1B,eAAe;;;cA6Fb,YAAS,aACR,UAAW,qBAAwB;;;;;cAqDpC,eAAY,aAAe,UAAW,qBAAwB"}
package/dist/sse.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { n as httpWithLive, r as liveFetch, t as LiveFetchOptions } from "./sse-C7LhdbiQ.js";
2
+ export { LiveFetchOptions, httpWithLive, liveFetch };
package/dist/sse.js ADDED
@@ -0,0 +1,127 @@
1
+ import { LiveChange, RemoteLiveError, RemoteLiveRefused, RemoteUnauthenticated } from "./wire.js";
2
+ import { http } from "./json.js";
3
+ import { Effect, Predicate, Schema, Stream } from "effect";
4
+ // src/sse.ts
5
+ /**
6
+ * Remote live over fetch with SSE framing: the other end of
7
+ * `foldkit-remote-server/fetch`'s live open. One `POST` carries the live
8
+ * requirement; the answer is a `text/event-stream` of `LiveChange` frames,
9
+ * one `data:` frame per change, closing when the server has nothing more or
10
+ * failing on a terminal `event: error` frame. Abandoning the stream aborts
11
+ * the request, which interrupts the subscription server-side.
12
+ *
13
+ * `httpWithLive` is this live stream beside `http`'s reads, queries, and
14
+ * mutations, for `Remote.clientLayer`.
15
+ */
16
+ const decodeChange = Schema.decodeUnknownSync(LiveChange);
17
+ const parseFrame = (text) => {
18
+ let event = "message";
19
+ const data = [];
20
+ for (const line of text.split(/\r\n|\n/)) {
21
+ if (line.startsWith(":") || line === "") continue;
22
+ const colon = line.indexOf(":");
23
+ if (colon < 0) continue;
24
+ const field = line.slice(0, colon);
25
+ const value = line.slice(colon + 1).startsWith(" ") ? line.slice(colon + 2) : line.slice(colon + 1);
26
+ if (field === "event") event = value;
27
+ else if (field === "data") data.push(value);
28
+ }
29
+ return {
30
+ event,
31
+ data
32
+ };
33
+ };
34
+ /** The frames of a body, split liberally: a blank line ends a frame. */
35
+ async function* frames(body) {
36
+ const reader = body.getReader();
37
+ const decoder = new TextDecoder();
38
+ let buffer = "";
39
+ try {
40
+ for (;;) {
41
+ const next = await reader.read();
42
+ if (next.done) break;
43
+ buffer += decoder.decode(next.value, { stream: true });
44
+ for (;;) {
45
+ const found = /(?:\r\n|\n){2}/.exec(buffer);
46
+ if (found === null) break;
47
+ yield parseFrame(buffer.slice(0, found.index));
48
+ buffer = buffer.slice(found.index + found[0].length);
49
+ }
50
+ }
51
+ buffer += decoder.decode();
52
+ if (buffer.trim() !== "") yield parseFrame(buffer);
53
+ } finally {
54
+ await reader.cancel().catch(() => {});
55
+ reader.releaseLock();
56
+ }
57
+ }
58
+ /** The wire changes of a live body: data frames decoded, error frames failed. */
59
+ async function* changes(body) {
60
+ for await (const frame of frames(body)) {
61
+ if (frame.data.length === 0) continue;
62
+ const text = frame.data.join("\n");
63
+ if (frame.event === "error") {
64
+ const said = JSON.parse(text);
65
+ const message = Predicate.isObject(said) && typeof said.message === "string" ? said.message : text;
66
+ if (Predicate.isObject(said) && Predicate.isObject(said.unauthenticated)) {
67
+ const reason = said.unauthenticated.reason;
68
+ throw new RemoteUnauthenticated({
69
+ message,
70
+ ...typeof reason === "string" ? { reason } : {}
71
+ });
72
+ }
73
+ if (Predicate.isObject(said) && said.refused === true) throw new RemoteLiveRefused({ message });
74
+ throw new RemoteLiveError({ message });
75
+ }
76
+ yield decodeChange(JSON.parse(text));
77
+ }
78
+ }
79
+ const liveError = (error) => error instanceof RemoteLiveError || error instanceof RemoteLiveRefused || error instanceof RemoteUnauthenticated ? error : new RemoteLiveError({ message: error instanceof Error ? error.message : String(error) });
80
+ /** Remote's live stream over fetch: `serveFetch`'s live open as a `Stream`. */
81
+ const liveFetch = (url, options = {}) => (payload) => Stream.unwrap(Effect.gen(function* () {
82
+ const via = options.fetch ?? fetch;
83
+ const controller = yield* Effect.acquireRelease(Effect.sync(() => new AbortController()), (open) => Effect.sync(() => open.abort()));
84
+ const response = yield* Effect.tryPromise({
85
+ try: () => via(url, {
86
+ method: "POST",
87
+ headers: {
88
+ "content-type": "application/json",
89
+ ...options.headers?.()
90
+ },
91
+ body: JSON.stringify({
92
+ operation: "live",
93
+ payload
94
+ }),
95
+ signal: controller.signal
96
+ }),
97
+ catch: liveError
98
+ });
99
+ if (!response.ok) {
100
+ const answered = yield* Effect.tryPromise({
101
+ try: () => response.json().catch(() => null),
102
+ catch: liveError
103
+ });
104
+ const message = Predicate.isObject(answered) && "error" in answered ? String(answered.error) : `HTTP ${response.status} ${response.statusText}`;
105
+ if (response.status === 401) {
106
+ const said = Predicate.isObject(answered) && Predicate.isObject(answered.unauthenticated) ? answered.unauthenticated.reason : void 0;
107
+ return yield* new RemoteUnauthenticated({
108
+ message,
109
+ ...typeof said === "string" ? { reason: said } : {}
110
+ });
111
+ }
112
+ return yield* new RemoteLiveError({ message });
113
+ }
114
+ if (response.body === null) return yield* new RemoteLiveError({ message: "The server answered live with no body" });
115
+ return Stream.fromAsyncIterable(changes(response.body), liveError);
116
+ }));
117
+ /**
118
+ * Remote's client over HTTP with live: reads, queries, and mutations as
119
+ * `http` sends them, live as `liveFetch` streams it, for `clientLayer`.
120
+ */
121
+ const httpWithLive = (url, options = {}) => ({
122
+ ...http(url, options),
123
+ FoldkitRemoteLive: liveFetch(url, options)
124
+ });
125
+ export { httpWithLive, liveFetch };
126
+
127
+ //# sourceMappingURL=sse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.js","names":[],"sources":["../src/sse.ts"],"sourcesContent":["/**\n * Remote live over fetch with SSE framing: the other end of\n * `foldkit-remote-server/fetch`'s live open. One `POST` carries the live\n * requirement; the answer is a `text/event-stream` of `LiveChange` frames,\n * one `data:` frame per change, closing when the server has nothing more or\n * failing on a terminal `event: error` frame. Abandoning the stream aborts\n * the request, which interrupts the subscription server-side.\n *\n * `httpWithLive` is this live stream beside `http`'s reads, queries, and\n * mutations, for `Remote.clientLayer`.\n */\nimport { Effect, Predicate, Schema, Stream } from 'effect'\nimport type { RemoteJsonClient, RemoteRpcClient } from './client.js'\nimport { http } from './json.js'\nimport {\n LiveChange,\n RemoteLiveError,\n RemoteLiveRefused,\n RemoteUnauthenticated,\n type LiveRequirement,\n} from './wire.js'\n\n/** What `httpWithLive` needs besides the URL. Mirrors `http`, plus the fetch itself. */\nexport interface LiveFetchOptions {\n /** Read per request, so a token that changes reaches the next call. */\n readonly headers?: (() => Readonly<Record<string, string>>) | undefined\n /** Defaults to the global one; pass it to route calls elsewhere in tests. */\n readonly fetch?: typeof fetch | undefined\n}\n\nconst decodeChange = Schema.decodeUnknownSync(LiveChange)\n\n/**\n * One SSE frame: its event (default `message`) and its data lines. Comment\n * and other lines decide nothing and are left out rather than kept.\n */\ninterface Frame {\n readonly event: string\n readonly data: ReadonlyArray<string>\n}\n\nconst parseFrame = (text: string): Frame => {\n let event = 'message'\n const data: string[] = []\n for (const line of text.split(/\\r\\n|\\n/)) {\n if (line.startsWith(':') || line === '') continue\n const colon = line.indexOf(':')\n if (colon < 0) continue\n const field = line.slice(0, colon)\n const value = line.slice(colon + 1).startsWith(' ')\n ? line.slice(colon + 2)\n : line.slice(colon + 1)\n if (field === 'event') event = value\n else if (field === 'data') data.push(value)\n }\n return { event, data }\n}\n\n/** The frames of a body, split liberally: a blank line ends a frame. */\nasync function* frames(body: ReadableStream<Uint8Array>): AsyncGenerator<Frame> {\n const reader = body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n try {\n for (;;) {\n const next = await reader.read()\n if (next.done) break\n buffer += decoder.decode(next.value, { stream: true })\n for (;;) {\n const found = /(?:\\r\\n|\\n){2}/.exec(buffer)\n if (found === null) break\n yield parseFrame(buffer.slice(0, found.index))\n buffer = buffer.slice(found.index + found[0].length)\n }\n }\n buffer += decoder.decode()\n if (buffer.trim() !== '') yield parseFrame(buffer)\n } finally {\n await reader.cancel().catch(() => {})\n reader.releaseLock()\n }\n}\n\n/** The wire changes of a live body: data frames decoded, error frames failed. */\nasync function* changes(\n body: ReadableStream<Uint8Array>,\n): AsyncGenerator<Schema.Schema.Type<typeof LiveChange>> {\n for await (const frame of frames(body)) {\n if (frame.data.length === 0) continue\n const text = frame.data.join('\\n')\n if (frame.event === 'error') {\n const said: unknown = JSON.parse(text)\n const message =\n Predicate.isObject(said) && typeof said.message === 'string' ? said.message : text\n // A stream ended because who it serves could no longer be told says so.\n if (Predicate.isObject(said) && Predicate.isObject(said.unauthenticated)) {\n const reason = said.unauthenticated.reason\n throw new RemoteUnauthenticated({\n message,\n ...(typeof reason === 'string' ? { reason } : {}),\n })\n }\n // Refused for what it asks: opening it again is refused again.\n if (Predicate.isObject(said) && said.refused === true) {\n throw new RemoteLiveRefused({ message })\n }\n throw new RemoteLiveError({ message })\n }\n yield decodeChange(JSON.parse(text))\n }\n}\n\nconst liveError = (error: unknown): RemoteLiveError | RemoteLiveRefused | RemoteUnauthenticated =>\n error instanceof RemoteLiveError ||\n error instanceof RemoteLiveRefused ||\n error instanceof RemoteUnauthenticated\n ? error\n : new RemoteLiveError({ message: error instanceof Error ? error.message : String(error) })\n\n/** Remote's live stream over fetch: `serveFetch`'s live open as a `Stream`. */\nexport const liveFetch =\n (url: string, options: LiveFetchOptions = {}): RemoteRpcClient['FoldkitRemoteLive'] =>\n (payload: Schema.Schema.Type<typeof LiveRequirement>) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const via = options.fetch ?? fetch\n const controller = yield* Effect.acquireRelease(\n Effect.sync(() => new AbortController()),\n open => Effect.sync(() => open.abort()),\n )\n const response = yield* Effect.tryPromise({\n try: () =>\n via(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...options.headers?.() },\n body: JSON.stringify({ operation: 'live', payload }),\n signal: controller.signal,\n }),\n catch: liveError,\n })\n if (!response.ok) {\n // A body that is not an answer still answers: null stands in for it.\n const answered = yield* Effect.tryPromise({\n try: () => response.json().catch(() => null) as Promise<unknown>,\n catch: liveError,\n })\n const message =\n Predicate.isObject(answered) && 'error' in answered\n ? String(answered.error)\n : `HTTP ${response.status} ${response.statusText}`\n if (response.status === 401) {\n // `serveFetch` says why under `unauthenticated`; middleware may say nothing.\n const said =\n Predicate.isObject(answered) && Predicate.isObject(answered.unauthenticated)\n ? answered.unauthenticated.reason\n : undefined\n return yield* new RemoteUnauthenticated({\n message,\n ...(typeof said === 'string' ? { reason: said } : {}),\n })\n }\n return yield* new RemoteLiveError({ message })\n }\n if (response.body === null) {\n return yield* new RemoteLiveError({ message: 'The server answered live with no body' })\n }\n return Stream.fromAsyncIterable(changes(response.body), liveError)\n }),\n )\n\n/**\n * Remote's client over HTTP with live: reads, queries, and mutations as\n * `http` sends them, live as `liveFetch` streams it, for `clientLayer`.\n */\nexport const httpWithLive = (url: string, options: LiveFetchOptions = {}): RemoteJsonClient => ({\n ...http(url, options),\n FoldkitRemoteLive: liveFetch(url, options),\n})\n"],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ import { Option } from "effect";
2
+ type EntityKey = string;
3
+ interface EntityEntry {
4
+ readonly values: Readonly<Record<string, unknown>>;
5
+ readonly present: ReadonlySet<string>;
6
+ /**
7
+ * Present fields whose value may be outdated (revalidation needed). On a
8
+ * tombstone, the fields it is being asked about again (`askAgain`).
9
+ */
10
+ readonly stale: ReadonlySet<string>;
11
+ /**
12
+ * Fields the server settled without a value: asked for, and answered without
13
+ * them, whatever the reason. Not planned again until a refresh asks; a later
14
+ * write of the field clears it.
15
+ */
16
+ readonly unavailable: ReadonlySet<string>;
17
+ /** The entity is known to be absent; a later write clears this. */
18
+ readonly tombstone: boolean;
19
+ /** Injected clock reading of the last write; never read from ambient state. */
20
+ readonly updatedAt: number;
21
+ /**
22
+ * The canonical window each present field was fetched with (`""` for none).
23
+ * A later request with a *known, different* window is planned as missing.
24
+ */
25
+ readonly windows: Readonly<Record<string, string>>;
26
+ }
27
+ type EntityStore = Readonly<Record<EntityKey, EntityEntry>>;
28
+ /** The separator keeps an untrusted id from colliding with a prototype key. */
29
+ declare const entityKey: (entity: string, id: string) => EntityKey;
30
+ declare const emptyStore: EntityStore;
31
+ interface EntityWrite {
32
+ readonly key: EntityKey;
33
+ readonly values: Readonly<Record<string, unknown>>;
34
+ /** The canonical window each written relation field was fetched with. */
35
+ readonly windows?: Readonly<Record<string, string>> | undefined;
36
+ /** When this write dates the entity, instead of the batch's `now`. */
37
+ readonly updatedAt?: number | undefined;
38
+ }
39
+ /**
40
+ * Merges known values into many entities, copying the store once. Every
41
+ * written field becomes present and non-stale; a tombstone is cleared because
42
+ * the entity is evidently not absent.
43
+ */
44
+ declare const writeEntities: (store: EntityStore, writes: ReadonlyArray<EntityWrite>, now?: number) => EntityStore;
45
+ /** `writeEntities` for one entity. */
46
+ declare const writeEntity: (store: EntityStore, key: EntityKey, values: Readonly<Record<string, unknown>>, now?: number, windows?: Readonly<Record<string, string>>) => EntityStore;
47
+ /**
48
+ * Marks present fields stale (`true`) or ends their staleness (`false`) across
49
+ * many entities, copying the store once. A field never present stays missing;
50
+ * an unknown entity is left alone.
51
+ */
52
+ declare const setStale: (store: EntityStore, marks: ReadonlyArray<readonly [key: EntityKey, fields: Iterable<string>]>, stale: boolean) => EntityStore;
53
+ /** Marks present fields stale; fields that were never present stay missing. */
54
+ declare const markStale: (store: EntityStore, key: EntityKey, fields: Iterable<string>) => EntityStore;
55
+ /** Ends a refresh that did not land: the fields are present again as they were. */
56
+ declare const clearStale: (store: EntityStore, key: EntityKey, fields: Iterable<string>) => EntityStore;
57
+ /**
58
+ * Records fields the server settled without a value. A present field is left
59
+ * as it is: the answer that settled it was not about the value it holds.
60
+ * `false` forgets the marks, which is how a refresh asks again.
61
+ */
62
+ declare const setUnavailable: (store: EntityStore, marks: ReadonlyArray<readonly [key: EntityKey, fields: Iterable<string>]>, unavailable: boolean) => EntityStore;
63
+ /** Whether the server settled this field without a value, and nothing has written it since. */
64
+ declare const isFieldUnavailable: (store: EntityStore, key: EntityKey, field: string) => boolean;
65
+ /**
66
+ * Records that the entity is known to be absent, so it is not refetched. On a
67
+ * tombstone being asked about again, the answer: still absent, so the asking ends.
68
+ */
69
+ declare const tombstone: (store: EntityStore, key: EntityKey) => EntityStore;
70
+ /**
71
+ * Asks about tombstoned entities again: each stays absent, so a list keeps
72
+ * hiding it and a read of it keeps `NotFound`, while `missingFields` plans the
73
+ * fields asked for. A write clears the tombstone; `tombstone` ends the asking.
74
+ * An entity that is not a tombstone is left alone.
75
+ */
76
+ declare const askAgain: (store: EntityStore, marks: ReadonlyArray<readonly [key: EntityKey, fields: Iterable<string>]>) => EntityStore;
77
+ /** Forgets everything known about the entity, including a tombstone. */
78
+ declare const remove: (store: EntityStore, key: EntityKey) => EntityStore;
79
+ declare const entry: (store: EntityStore, key: EntityKey) => Option.Option<EntityEntry>;
80
+ declare const isTombstone: (store: EntityStore, key: EntityKey) => boolean;
81
+ /** A field is known only if it is present and not stale. */
82
+ declare const hasField: (store: EntityStore, key: EntityKey, field: string) => boolean;
83
+ /** A present field whose value may be outdated; absent fields are not stale. */
84
+ declare const isFieldStale: (store: EntityStore, key: EntityKey, field: string) => boolean;
85
+ /**
86
+ * Reads a present field, stale or not. Absence and a tombstone both yield
87
+ * `Option.none()`; a present `undefined`/`null` yields `Option.some`.
88
+ */
89
+ declare const readField: (store: EntityStore, key: EntityKey, field: string) => Option.Option<unknown>;
90
+ /**
91
+ * The fields the planner must fetch. A tombstone makes every field known
92
+ * (absent), so it returns only what it is asked about again, and a
93
+ * field the server settled without a value is known the same way. A
94
+ * requested window that differs from the one a field was fetched with also
95
+ * marks it missing, but only when the stored window is known (never `""`), so a
96
+ * writer that does not record windows cannot cause a refetch loop.
97
+ */
98
+ declare const missingFields: (store: EntityStore, key: EntityKey, fields: Iterable<string>, windows?: Readonly<Record<string, string>>) => ReadonlyArray<string>;
99
+ export { writeEntity as S, remove as _, askAgain as a, tombstone as b, entityKey as c, isFieldStale as d, isFieldUnavailable as f, readField as g, missingFields as h, EntityWrite as i, entry as l, markStale as m, EntityKey as n, clearStale as o, isTombstone as p, EntityStore as r, emptyStore as s, EntityEntry as t, hasField as u, setStale as v, writeEntities as x, setUnavailable as y };
100
+ //# sourceMappingURL=store-ah2qjcJW.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store-ah2qjcJW.d.ts","names":[],"sources":["../src/store.ts"],"mappings":";KAaY;UAEK;WACN,QAAQ,SAAS;WACjB,SAAS;;;;;WAKT,OAAO;;;;;;WAMP,aAAa;;WAEb;;WAEA;;;;;WAKA,SAAS,SAAS;;KAGjB,cAAc,SAAS,OAAO,WAAW;;cAGxC,YAAS,gBAAkB,eAAe;cAY1C,YAAY;UAiDR;WACN,KAAK;WACL,QAAQ,SAAS;;WAEjB,UAAU,SAAS;;WAEnB;;;;;;;cAQE,gBAAa,OACjB,aAAW,QACV,cAAc,cAAY,iBAEjC;;cAeU,cAAW,OACf,aAAW,KACb,WAAS,QACN,SAAS,0BAAwB,cAAA,UAE/B,SAAS,4BAClB;;;;;;cAOU,WAAQ,OACZ,aAAW,OACX,wBAAwB,KAAK,WAAW,QAAQ,oBAAkB,mBAExE;;cAoBU,YAAS,OACb,aAAW,KACb,WAAS,QACN,qBACP;;cAGU,aAAU,OACd,aAAW,KACb,WAAS,QACN,qBACP;;;;;;cAOU,iBAAc,OAClB,aAAW,OACX,wBAAwB,KAAK,WAAW,QAAQ,oBAAkB,yBAExE;;cAmBU,qBAAkB,OAAW,aAAW,KAAO,WAAS;;;;;cASxD,YAAS,OAAW,aAAW,KAAO,cAAY;;;;;;;cAmBlD,WAAQ,OACZ,aAAW,OACX,wBAAwB,KAAK,WAAW,QAAQ,uBACtD;;cAcU,SAAM,OAAW,aAAW,KAAO,cAAY;cAO/C,QAAK,OAAW,aAAW,KAAO,cAAY,OAAO,OAAO;cAK5D,cAAW,OAAW,aAAW,KAAO;;cAIxC,WAAQ,OAAW,aAAW,KAAO,WAAS;;cAQ9C,eAAY,OAAW,aAAW,KAAO,WAAS;;;;;cASlD,YAAS,OACb,aAAW,KACb,WAAS,kBAEb,OAAO;;;;;;;;;cAcG,gBAAa,OACjB,aAAW,KACb,WAAS,QACN,kBAAgB,UACd,SAAS,4BAClB"}
@@ -0,0 +1,2 @@
1
+ import { S as writeEntity, _ as remove, a as askAgain, b as tombstone, c as entityKey, d as isFieldStale, f as isFieldUnavailable, g as readField, h as missingFields, i as EntityWrite, l as entry, m as markStale, n as EntityKey, o as clearStale, p as isTombstone, r as EntityStore, s as emptyStore, t as EntityEntry, u as hasField, v as setStale, x as writeEntities, y as setUnavailable } from "./store-ah2qjcJW.js";
2
+ export { EntityEntry, EntityKey, EntityStore, EntityWrite, askAgain, clearStale, emptyStore, entityKey, entry, hasField, isFieldStale, isFieldUnavailable, isTombstone, markStale, missingFields, readField, remove, setStale, setUnavailable, tombstone, writeEntities, writeEntity };
package/dist/store.js ADDED
@@ -0,0 +1,218 @@
1
+ import { own, put } from "./record.js";
2
+ import { sameData } from "./data.js";
3
+ import "./relation.js";
4
+ import { Option } from "effect";
5
+ // src/store.ts
6
+ /**
7
+ * The pure entity store: values, per-field presence, staleness, and tombstones.
8
+ *
9
+ * Presence is tracked **separately** from values, so `undefined`/`null`/absent/
10
+ * stale/not-found are distinct states and a missing field is never inferred from
11
+ * `value === undefined`. Nothing here performs I/O or mutates its input; an
12
+ * operation that changes nothing returns the store it was given.
13
+ */
14
+ /** The separator keeps an untrusted id from colliding with a prototype key. */
15
+ const entityKey = (entity, id) => `${entity}:${id}`;
16
+ const emptyEntry = {
17
+ values: {},
18
+ present: /* @__PURE__ */ new Set(),
19
+ stale: /* @__PURE__ */ new Set(),
20
+ unavailable: /* @__PURE__ */ new Set(),
21
+ tombstone: false,
22
+ updatedAt: 0,
23
+ windows: {}
24
+ };
25
+ const emptyStore = {};
26
+ const replace = (store, key, entry) => ({
27
+ ...store,
28
+ [key]: entry
29
+ });
30
+ const written = (previous, values, now, windows) => {
31
+ const present = new Set(previous.present);
32
+ const stale = new Set(previous.stale);
33
+ const unavailable = new Set(previous.unavailable);
34
+ const nextWindows = { ...previous.windows };
35
+ for (const field of Object.keys(values)) {
36
+ present.add(field);
37
+ stale.delete(field);
38
+ unavailable.delete(field);
39
+ const requested = own(windows, field);
40
+ if (requested !== void 0 && requested !== "") put(nextWindows, field, requested);
41
+ else delete nextWindows[field];
42
+ if (!field.includes("@")) {
43
+ for (const held of present) if (held.startsWith(`${field}@`) && !(held in values)) stale.add(held);
44
+ }
45
+ }
46
+ return {
47
+ values: Object.keys(values).every((field) => Object.hasOwn(previous.values, field) && sameData(previous.values[field], values[field])) ? previous.values : {
48
+ ...previous.values,
49
+ ...values
50
+ },
51
+ present,
52
+ stale,
53
+ unavailable,
54
+ tombstone: false,
55
+ updatedAt: now,
56
+ windows: nextWindows
57
+ };
58
+ };
59
+ /**
60
+ * Merges known values into many entities, copying the store once. Every
61
+ * written field becomes present and non-stale; a tombstone is cleared because
62
+ * the entity is evidently not absent.
63
+ */
64
+ const writeEntities = (store, writes, now = 0) => {
65
+ if (writes.length === 0) return store;
66
+ const next = { ...store };
67
+ for (const write of writes) next[write.key] = written(next[write.key] ?? emptyEntry, write.values, write.updatedAt ?? now, write.windows);
68
+ return next;
69
+ };
70
+ /** `writeEntities` for one entity. */
71
+ const writeEntity = (store, key, values, now = 0, windows) => writeEntities(store, [{
72
+ key,
73
+ values,
74
+ windows
75
+ }], now);
76
+ /**
77
+ * Marks present fields stale (`true`) or ends their staleness (`false`) across
78
+ * many entities, copying the store once. A field never present stays missing;
79
+ * an unknown entity is left alone.
80
+ */
81
+ const setStale = (store, marks, stale) => {
82
+ let next;
83
+ for (const [key, fields] of marks) {
84
+ const previous = (next ?? store)[key];
85
+ if (previous === void 0) continue;
86
+ const staleFields = new Set(previous.stale);
87
+ for (const field of fields) if (stale) {
88
+ if (previous.present.has(field)) staleFields.add(field);
89
+ } else staleFields.delete(field);
90
+ if (staleFields.size === previous.stale.size) continue;
91
+ next ??= { ...store };
92
+ next[key] = {
93
+ ...previous,
94
+ stale: staleFields
95
+ };
96
+ }
97
+ return next ?? store;
98
+ };
99
+ /** Marks present fields stale; fields that were never present stay missing. */
100
+ const markStale = (store, key, fields) => setStale(store, [[key, fields]], true);
101
+ /** Ends a refresh that did not land: the fields are present again as they were. */
102
+ const clearStale = (store, key, fields) => setStale(store, [[key, fields]], false);
103
+ /**
104
+ * Records fields the server settled without a value. A present field is left
105
+ * as it is: the answer that settled it was not about the value it holds.
106
+ * `false` forgets the marks, which is how a refresh asks again.
107
+ */
108
+ const setUnavailable = (store, marks, unavailable) => {
109
+ let next;
110
+ for (const [key, fields] of marks) {
111
+ const previous = (next ?? store)[key] ?? emptyEntry;
112
+ if (previous.tombstone) continue;
113
+ const marked = new Set(previous.unavailable);
114
+ for (const field of fields) if (unavailable) {
115
+ if (!previous.present.has(field)) marked.add(field);
116
+ } else marked.delete(field);
117
+ if (marked.size === previous.unavailable.size) continue;
118
+ next ??= { ...store };
119
+ next[key] = {
120
+ ...previous,
121
+ unavailable: marked
122
+ };
123
+ }
124
+ return next ?? store;
125
+ };
126
+ /** Whether the server settled this field without a value, and nothing has written it since. */
127
+ const isFieldUnavailable = (store, key, field) => {
128
+ const value = store[key];
129
+ return value !== void 0 && !value.tombstone && value.unavailable.has(field);
130
+ };
131
+ /**
132
+ * Records that the entity is known to be absent, so it is not refetched. On a
133
+ * tombstone being asked about again, the answer: still absent, so the asking ends.
134
+ */
135
+ const tombstone = (store, key) => isTombstone(store, key) && store[key].stale.size === 0 ? store : replace(store, key, {
136
+ values: {},
137
+ present: /* @__PURE__ */ new Set(),
138
+ stale: /* @__PURE__ */ new Set(),
139
+ unavailable: /* @__PURE__ */ new Set(),
140
+ tombstone: true,
141
+ updatedAt: 0,
142
+ windows: {}
143
+ });
144
+ /**
145
+ * Asks about tombstoned entities again: each stays absent, so a list keeps
146
+ * hiding it and a read of it keeps `NotFound`, while `missingFields` plans the
147
+ * fields asked for. A write clears the tombstone; `tombstone` ends the asking.
148
+ * An entity that is not a tombstone is left alone.
149
+ */
150
+ const askAgain = (store, marks) => {
151
+ let next;
152
+ for (const [key, fields] of marks) {
153
+ const previous = (next ?? store)[key];
154
+ if (previous === void 0 || !previous.tombstone) continue;
155
+ const asked = /* @__PURE__ */ new Set([...previous.stale, ...fields]);
156
+ if (asked.size === previous.stale.size) continue;
157
+ next ??= { ...store };
158
+ next[key] = {
159
+ ...previous,
160
+ stale: asked
161
+ };
162
+ }
163
+ return next ?? store;
164
+ };
165
+ /** Forgets everything known about the entity, including a tombstone. */
166
+ const remove = (store, key) => {
167
+ if (store[key] === void 0) return store;
168
+ const next = { ...store };
169
+ delete next[key];
170
+ return next;
171
+ };
172
+ const entry = (store, key) => {
173
+ const value = store[key];
174
+ return value === void 0 ? Option.none() : Option.some(value);
175
+ };
176
+ const isTombstone = (store, key) => store[key]?.tombstone === true;
177
+ /** A field is known only if it is present and not stale. */
178
+ const hasField = (store, key, field) => {
179
+ const value = store[key];
180
+ return value !== void 0 && !value.tombstone && value.present.has(field) && !value.stale.has(field);
181
+ };
182
+ /** A present field whose value may be outdated; absent fields are not stale. */
183
+ const isFieldStale = (store, key, field) => {
184
+ const value = store[key];
185
+ return value !== void 0 && !value.tombstone && value.stale.has(field);
186
+ };
187
+ /**
188
+ * Reads a present field, stale or not. Absence and a tombstone both yield
189
+ * `Option.none()`; a present `undefined`/`null` yields `Option.some`.
190
+ */
191
+ const readField = (store, key, field) => {
192
+ const value = store[key];
193
+ if (value === void 0 || value.tombstone || !value.present.has(field)) return Option.none();
194
+ return Option.some(value.values[field]);
195
+ };
196
+ /**
197
+ * The fields the planner must fetch. A tombstone makes every field known
198
+ * (absent), so it returns only what it is asked about again, and a
199
+ * field the server settled without a value is known the same way. A
200
+ * requested window that differs from the one a field was fetched with also
201
+ * marks it missing, but only when the stored window is known (never `""`), so a
202
+ * writer that does not record windows cannot cause a refetch loop.
203
+ */
204
+ const missingFields = (store, key, fields, windows) => {
205
+ const value = store[key];
206
+ if (value !== void 0 && value.tombstone) return [...fields].filter((f) => value.stale.has(f));
207
+ return [...fields].filter((field) => {
208
+ if (value === void 0) return true;
209
+ if (!value.present.has(field)) return !value.unavailable.has(field);
210
+ if (value.stale.has(field)) return true;
211
+ const requested = own(windows, field);
212
+ const stored = own(value.windows, field);
213
+ return requested !== void 0 && stored !== void 0 && stored !== "" && stored !== requested;
214
+ });
215
+ };
216
+ export { askAgain, clearStale, emptyStore, entityKey, entry, hasField, isFieldStale, isFieldUnavailable, isTombstone, markStale, missingFields, readField, remove, setStale, setUnavailable, tombstone, writeEntities, writeEntity };
217
+
218
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","names":[],"sources":["../src/store.ts"],"sourcesContent":["/**\n * The pure entity store: values, per-field presence, staleness, and tombstones.\n *\n * Presence is tracked **separately** from values, so `undefined`/`null`/absent/\n * stale/not-found are distinct states and a missing field is never inferred from\n * `value === undefined`. Nothing here performs I/O or mutates its input; an\n * operation that changes nothing returns the store it was given.\n */\nimport { Option } from 'effect'\nimport { sameData } from './data.js'\nimport { own, put } from './record.js'\nimport { RELATION_ALIAS } from './relation.js'\n\nexport type EntityKey = string\n\nexport interface EntityEntry {\n readonly values: Readonly<Record<string, unknown>>\n readonly present: ReadonlySet<string>\n /**\n * Present fields whose value may be outdated (revalidation needed). On a\n * tombstone, the fields it is being asked about again (`askAgain`).\n */\n readonly stale: ReadonlySet<string>\n /**\n * Fields the server settled without a value: asked for, and answered without\n * them, whatever the reason. Not planned again until a refresh asks; a later\n * write of the field clears it.\n */\n readonly unavailable: ReadonlySet<string>\n /** The entity is known to be absent; a later write clears this. */\n readonly tombstone: boolean\n /** Injected clock reading of the last write; never read from ambient state. */\n readonly updatedAt: number\n /**\n * The canonical window each present field was fetched with (`\"\"` for none).\n * A later request with a *known, different* window is planned as missing.\n */\n readonly windows: Readonly<Record<string, string>>\n}\n\nexport type EntityStore = Readonly<Record<EntityKey, EntityEntry>>\n\n/** The separator keeps an untrusted id from colliding with a prototype key. */\nexport const entityKey = (entity: string, id: string): EntityKey => `${entity}:${id}`\n\nconst emptyEntry: EntityEntry = {\n values: {},\n present: new Set(),\n stale: new Set(),\n unavailable: new Set(),\n tombstone: false,\n updatedAt: 0,\n windows: {},\n}\n\nexport const emptyStore: EntityStore = {}\n\nconst replace = (store: EntityStore, key: EntityKey, entry: EntityEntry): EntityStore => ({\n ...store,\n [key]: entry,\n})\n\nconst written = (\n previous: EntityEntry,\n values: Readonly<Record<string, unknown>>,\n now: number,\n windows?: Readonly<Record<string, string>>,\n): EntityEntry => {\n const present = new Set(previous.present)\n const stale = new Set(previous.stale)\n const unavailable = new Set(previous.unavailable)\n const nextWindows: Record<string, string> = { ...previous.windows }\n for (const field of Object.keys(values)) {\n present.add(field)\n stale.delete(field)\n unavailable.delete(field)\n // A write without a window clears any remembered one: the value changed.\n const requested = own(windows, field)\n if (requested !== undefined && requested !== '') put(nextWindows, field, requested)\n else delete nextWindows[field]\n // The whole list changed, so a page of it read under an alias may have too.\n if (!field.includes(RELATION_ALIAS))\n for (const held of present)\n if (held.startsWith(`${field}${RELATION_ALIAS}`) && !(held in values)) stale.add(held)\n }\n return {\n // Equal data keeps the object it was, so what is derived from it by\n // identity (a read's decoded row) survives a refetch that changed nothing.\n // Comparing costs the size of the written data, less than decoding it again.\n values: Object.keys(values).every(\n field =>\n Object.hasOwn(previous.values, field) && sameData(previous.values[field], values[field]),\n )\n ? previous.values\n : { ...previous.values, ...values },\n present,\n stale,\n unavailable,\n tombstone: false,\n updatedAt: now,\n windows: nextWindows,\n }\n}\n\nexport interface EntityWrite {\n readonly key: EntityKey\n readonly values: Readonly<Record<string, unknown>>\n /** The canonical window each written relation field was fetched with. */\n readonly windows?: Readonly<Record<string, string>> | undefined\n /** When this write dates the entity, instead of the batch's `now`. */\n readonly updatedAt?: number | undefined\n}\n\n/**\n * Merges known values into many entities, copying the store once. Every\n * written field becomes present and non-stale; a tombstone is cleared because\n * the entity is evidently not absent.\n */\nexport const writeEntities = (\n store: EntityStore,\n writes: ReadonlyArray<EntityWrite>,\n now = 0,\n): EntityStore => {\n if (writes.length === 0) return store\n const next: Record<EntityKey, EntityEntry> = { ...store }\n for (const write of writes) {\n next[write.key] = written(\n next[write.key] ?? emptyEntry,\n write.values,\n write.updatedAt ?? now,\n write.windows,\n )\n }\n return next\n}\n\n/** `writeEntities` for one entity. */\nexport const writeEntity = (\n store: EntityStore,\n key: EntityKey,\n values: Readonly<Record<string, unknown>>,\n now = 0,\n windows?: Readonly<Record<string, string>>,\n): EntityStore => writeEntities(store, [{ key, values, windows }], now)\n\n/**\n * Marks present fields stale (`true`) or ends their staleness (`false`) across\n * many entities, copying the store once. A field never present stays missing;\n * an unknown entity is left alone.\n */\nexport const setStale = (\n store: EntityStore,\n marks: ReadonlyArray<readonly [key: EntityKey, fields: Iterable<string>]>,\n stale: boolean,\n): EntityStore => {\n let next: Record<EntityKey, EntityEntry> | undefined\n for (const [key, fields] of marks) {\n const previous = (next ?? store)[key]\n if (previous === undefined) continue\n const staleFields = new Set(previous.stale)\n for (const field of fields) {\n if (stale) {\n if (previous.present.has(field)) staleFields.add(field)\n } else staleFields.delete(field)\n }\n // Only ever added to or only ever removed from, so an equal size is the same set.\n if (staleFields.size === previous.stale.size) continue\n next ??= { ...store }\n next[key] = { ...previous, stale: staleFields }\n }\n return next ?? store\n}\n\n/** Marks present fields stale; fields that were never present stay missing. */\nexport const markStale = (\n store: EntityStore,\n key: EntityKey,\n fields: Iterable<string>,\n): EntityStore => setStale(store, [[key, fields]], true)\n\n/** Ends a refresh that did not land: the fields are present again as they were. */\nexport const clearStale = (\n store: EntityStore,\n key: EntityKey,\n fields: Iterable<string>,\n): EntityStore => setStale(store, [[key, fields]], false)\n\n/**\n * Records fields the server settled without a value. A present field is left\n * as it is: the answer that settled it was not about the value it holds.\n * `false` forgets the marks, which is how a refresh asks again.\n */\nexport const setUnavailable = (\n store: EntityStore,\n marks: ReadonlyArray<readonly [key: EntityKey, fields: Iterable<string>]>,\n unavailable: boolean,\n): EntityStore => {\n let next: Record<EntityKey, EntityEntry> | undefined\n for (const [key, fields] of marks) {\n const previous = (next ?? store)[key] ?? emptyEntry\n if (previous.tombstone) continue\n const marked = new Set(previous.unavailable)\n for (const field of fields) {\n if (unavailable) {\n if (!previous.present.has(field)) marked.add(field)\n } else marked.delete(field)\n }\n if (marked.size === previous.unavailable.size) continue\n next ??= { ...store }\n next[key] = { ...previous, unavailable: marked }\n }\n return next ?? store\n}\n\n/** Whether the server settled this field without a value, and nothing has written it since. */\nexport const isFieldUnavailable = (store: EntityStore, key: EntityKey, field: string): boolean => {\n const value = store[key]\n return value !== undefined && !value.tombstone && value.unavailable.has(field)\n}\n\n/**\n * Records that the entity is known to be absent, so it is not refetched. On a\n * tombstone being asked about again, the answer: still absent, so the asking ends.\n */\nexport const tombstone = (store: EntityStore, key: EntityKey): EntityStore =>\n isTombstone(store, key) && store[key]!.stale.size === 0\n ? store\n : replace(store, key, {\n values: {},\n present: new Set(),\n stale: new Set(),\n unavailable: new Set(),\n tombstone: true,\n updatedAt: 0,\n windows: {},\n })\n\n/**\n * Asks about tombstoned entities again: each stays absent, so a list keeps\n * hiding it and a read of it keeps `NotFound`, while `missingFields` plans the\n * fields asked for. A write clears the tombstone; `tombstone` ends the asking.\n * An entity that is not a tombstone is left alone.\n */\nexport const askAgain = (\n store: EntityStore,\n marks: ReadonlyArray<readonly [key: EntityKey, fields: Iterable<string>]>,\n): EntityStore => {\n let next: Record<EntityKey, EntityEntry> | undefined\n for (const [key, fields] of marks) {\n const previous = (next ?? store)[key]\n if (previous === undefined || !previous.tombstone) continue\n const asked = new Set([...previous.stale, ...fields])\n if (asked.size === previous.stale.size) continue\n next ??= { ...store }\n next[key] = { ...previous, stale: asked }\n }\n return next ?? store\n}\n\n/** Forgets everything known about the entity, including a tombstone. */\nexport const remove = (store: EntityStore, key: EntityKey): EntityStore => {\n if (store[key] === undefined) return store\n const next = { ...store }\n delete next[key]\n return next\n}\n\nexport const entry = (store: EntityStore, key: EntityKey): Option.Option<EntityEntry> => {\n const value = store[key]\n return value === undefined ? Option.none() : Option.some(value)\n}\n\nexport const isTombstone = (store: EntityStore, key: EntityKey): boolean =>\n store[key]?.tombstone === true\n\n/** A field is known only if it is present and not stale. */\nexport const hasField = (store: EntityStore, key: EntityKey, field: string): boolean => {\n const value = store[key]\n return (\n value !== undefined && !value.tombstone && value.present.has(field) && !value.stale.has(field)\n )\n}\n\n/** A present field whose value may be outdated; absent fields are not stale. */\nexport const isFieldStale = (store: EntityStore, key: EntityKey, field: string): boolean => {\n const value = store[key]\n return value !== undefined && !value.tombstone && value.stale.has(field)\n}\n\n/**\n * Reads a present field, stale or not. Absence and a tombstone both yield\n * `Option.none()`; a present `undefined`/`null` yields `Option.some`.\n */\nexport const readField = (\n store: EntityStore,\n key: EntityKey,\n field: string,\n): Option.Option<unknown> => {\n const value = store[key]\n if (value === undefined || value.tombstone || !value.present.has(field)) return Option.none()\n return Option.some(value.values[field])\n}\n\n/**\n * The fields the planner must fetch. A tombstone makes every field known\n * (absent), so it returns only what it is asked about again, and a\n * field the server settled without a value is known the same way. A\n * requested window that differs from the one a field was fetched with also\n * marks it missing, but only when the stored window is known (never `\"\"`), so a\n * writer that does not record windows cannot cause a refetch loop.\n */\nexport const missingFields = (\n store: EntityStore,\n key: EntityKey,\n fields: Iterable<string>,\n windows?: Readonly<Record<string, string>>,\n): ReadonlyArray<string> => {\n const value = store[key]\n if (value !== undefined && value.tombstone) return [...fields].filter(f => value.stale.has(f))\n return [...fields].filter(field => {\n if (value === undefined) return true\n if (!value.present.has(field)) return !value.unavailable.has(field)\n if (value.stale.has(field)) return true\n const requested = own(windows, field)\n const stored = own(value.windows, field)\n return requested !== undefined && stored !== undefined && stored !== '' && stored !== requested\n })\n}\n"],"mappings":"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"}