@lssm/lib.contracts-runtime-core 3.0.1

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 (227) hide show
  1. package/README.md +110 -0
  2. package/dist/approval.d.ts +13 -0
  3. package/dist/approval.js +2 -0
  4. package/dist/approval.test.d.ts +1 -0
  5. package/dist/browser/approval.js +1 -0
  6. package/dist/browser/collaboration.js +0 -0
  7. package/dist/browser/contracts-runtime-core.feature.js +1 -0
  8. package/dist/browser/engine/backoff.js +1 -0
  9. package/dist/browser/engine/cache-store.js +1 -0
  10. package/dist/browser/engine/conflict-registry.js +1 -0
  11. package/dist/browser/engine/conflict-resolution.js +1 -0
  12. package/dist/browser/engine/data-engine-host.js +1 -0
  13. package/dist/browser/engine/data-engine-test-helpers.js +1 -0
  14. package/dist/browser/engine/data-engine.js +1 -0
  15. package/dist/browser/engine/dedup.js +1 -0
  16. package/dist/browser/engine/events.js +1 -0
  17. package/dist/browser/engine/index.js +1 -0
  18. package/dist/browser/engine/maintenance.js +1 -0
  19. package/dist/browser/engine/mutation-engine.js +1 -0
  20. package/dist/browser/engine/mutation-queue.js +1 -0
  21. package/dist/browser/engine/mutation-types.js +0 -0
  22. package/dist/browser/engine/no-cache-store.js +1 -0
  23. package/dist/browser/engine/online-only-data-engine.js +1 -0
  24. package/dist/browser/engine/online-only-mutation-engine.js +1 -0
  25. package/dist/browser/engine/query-engine.js +1 -0
  26. package/dist/browser/engine/state.js +1 -0
  27. package/dist/browser/engine/types.js +0 -0
  28. package/dist/browser/forms-platform-csv-columns.js +1 -0
  29. package/dist/browser/forms-platform-csv.js +2 -0
  30. package/dist/browser/forms-platform-management.js +1 -0
  31. package/dist/browser/forms-platform-navigation.js +1 -0
  32. package/dist/browser/forms-platform-submission.js +1 -0
  33. package/dist/browser/forms-platform-types.js +1 -0
  34. package/dist/browser/forms-platform-utils.js +1 -0
  35. package/dist/browser/forms-platform.js +2 -0
  36. package/dist/browser/index.js +3 -0
  37. package/dist/browser/local-offline.js +1 -0
  38. package/dist/browser/observability.js +1 -0
  39. package/dist/browser/reachability.js +1 -0
  40. package/dist/browser/redaction.js +1 -0
  41. package/dist/browser/storage/indexeddb.web.js +1 -0
  42. package/dist/browser/storage/keyed-store.js +1 -0
  43. package/dist/browser/storage/memory.js +1 -0
  44. package/dist/browser/storage/port.js +1 -0
  45. package/dist/browser/transport.js +2 -0
  46. package/dist/collaboration.d.ts +57 -0
  47. package/dist/collaboration.js +1 -0
  48. package/dist/contracts-runtime-core.feature.d.ts +1 -0
  49. package/dist/contracts-runtime-core.feature.js +2 -0
  50. package/dist/engine/backoff.d.ts +16 -0
  51. package/dist/engine/backoff.js +2 -0
  52. package/dist/engine/backoff.test.d.ts +1 -0
  53. package/dist/engine/cache-store.d.ts +19 -0
  54. package/dist/engine/cache-store.js +2 -0
  55. package/dist/engine/conflict-registry.d.ts +10 -0
  56. package/dist/engine/conflict-registry.js +2 -0
  57. package/dist/engine/conflict-registry.test.d.ts +1 -0
  58. package/dist/engine/conflict-resolution.d.ts +24 -0
  59. package/dist/engine/conflict-resolution.js +2 -0
  60. package/dist/engine/conflict-resolution.test.d.ts +1 -0
  61. package/dist/engine/data-engine-host.d.ts +30 -0
  62. package/dist/engine/data-engine-host.js +2 -0
  63. package/dist/engine/data-engine-local-offline.test.d.ts +1 -0
  64. package/dist/engine/data-engine-test-helpers.d.ts +30 -0
  65. package/dist/engine/data-engine-test-helpers.js +2 -0
  66. package/dist/engine/data-engine.d.ts +46 -0
  67. package/dist/engine/data-engine.js +2 -0
  68. package/dist/engine/data-engine.test.d.ts +1 -0
  69. package/dist/engine/dedup.d.ts +9 -0
  70. package/dist/engine/dedup.js +2 -0
  71. package/dist/engine/events.d.ts +76 -0
  72. package/dist/engine/events.economic-evidence.test.d.ts +1 -0
  73. package/dist/engine/events.js +2 -0
  74. package/dist/engine/index.d.ts +17 -0
  75. package/dist/engine/index.js +2 -0
  76. package/dist/engine/maintenance.d.ts +18 -0
  77. package/dist/engine/maintenance.js +2 -0
  78. package/dist/engine/maintenance.test.d.ts +1 -0
  79. package/dist/engine/mutation-engine.d.ts +2 -0
  80. package/dist/engine/mutation-engine.js +2 -0
  81. package/dist/engine/mutation-queue.d.ts +16 -0
  82. package/dist/engine/mutation-queue.js +2 -0
  83. package/dist/engine/mutation-queue.test.d.ts +1 -0
  84. package/dist/engine/mutation-types.d.ts +118 -0
  85. package/dist/engine/mutation-types.js +1 -0
  86. package/dist/engine/no-cache-store.d.ts +3 -0
  87. package/dist/engine/no-cache-store.js +2 -0
  88. package/dist/engine/online-only-data-engine.d.ts +9 -0
  89. package/dist/engine/online-only-data-engine.js +2 -0
  90. package/dist/engine/online-only-mutation-engine.d.ts +3 -0
  91. package/dist/engine/online-only-mutation-engine.js +2 -0
  92. package/dist/engine/query-engine.d.ts +7 -0
  93. package/dist/engine/query-engine.js +2 -0
  94. package/dist/engine/query-engine.test.d.ts +1 -0
  95. package/dist/engine/state.d.ts +37 -0
  96. package/dist/engine/state.js +2 -0
  97. package/dist/engine/types.d.ts +70 -0
  98. package/dist/engine/types.js +1 -0
  99. package/dist/forms-platform-choice.test.d.ts +1 -0
  100. package/dist/forms-platform-csv-columns.d.ts +7 -0
  101. package/dist/forms-platform-csv-columns.js +2 -0
  102. package/dist/forms-platform-csv.d.ts +12 -0
  103. package/dist/forms-platform-csv.js +3 -0
  104. package/dist/forms-platform-management.d.ts +12 -0
  105. package/dist/forms-platform-management.js +2 -0
  106. package/dist/forms-platform-navigation.d.ts +2 -0
  107. package/dist/forms-platform-navigation.js +2 -0
  108. package/dist/forms-platform-submission.d.ts +7 -0
  109. package/dist/forms-platform-submission.js +2 -0
  110. package/dist/forms-platform-types.d.ts +51 -0
  111. package/dist/forms-platform-types.js +2 -0
  112. package/dist/forms-platform-utils.d.ts +6 -0
  113. package/dist/forms-platform-utils.js +2 -0
  114. package/dist/forms-platform-utils.test.d.ts +1 -0
  115. package/dist/forms-platform.d.ts +19 -0
  116. package/dist/forms-platform.js +3 -0
  117. package/dist/forms-platform.test.d.ts +1 -0
  118. package/dist/index.d.ts +25 -0
  119. package/dist/index.js +4 -0
  120. package/dist/local-offline.d.ts +17 -0
  121. package/dist/local-offline.js +2 -0
  122. package/dist/local-offline.test.d.ts +1 -0
  123. package/dist/native/approval.js +130 -0
  124. package/dist/native/collaboration.js +0 -0
  125. package/dist/native/contracts-runtime-core.feature.js +17 -0
  126. package/dist/native/engine/backoff.js +16 -0
  127. package/dist/native/engine/cache-store.js +49 -0
  128. package/dist/native/engine/conflict-registry.js +51 -0
  129. package/dist/native/engine/conflict-resolution.js +81 -0
  130. package/dist/native/engine/data-engine-host.js +1119 -0
  131. package/dist/native/engine/data-engine-test-helpers.js +117 -0
  132. package/dist/native/engine/data-engine.js +1106 -0
  133. package/dist/native/engine/dedup.js +22 -0
  134. package/dist/native/engine/events.js +19 -0
  135. package/dist/native/engine/index.js +1189 -0
  136. package/dist/native/engine/maintenance.js +36 -0
  137. package/dist/native/engine/mutation-engine.js +360 -0
  138. package/dist/native/engine/mutation-queue.js +26 -0
  139. package/dist/native/engine/mutation-types.js +0 -0
  140. package/dist/native/engine/no-cache-store.js +15 -0
  141. package/dist/native/engine/online-only-data-engine.js +422 -0
  142. package/dist/native/engine/online-only-mutation-engine.js +70 -0
  143. package/dist/native/engine/query-engine.js +360 -0
  144. package/dist/native/engine/state.js +116 -0
  145. package/dist/native/engine/types.js +0 -0
  146. package/dist/native/forms-platform-csv-columns.js +31 -0
  147. package/dist/native/forms-platform-csv.js +150 -0
  148. package/dist/native/forms-platform-management.js +179 -0
  149. package/dist/native/forms-platform-navigation.js +27 -0
  150. package/dist/native/forms-platform-submission.js +207 -0
  151. package/dist/native/forms-platform-types.js +12 -0
  152. package/dist/native/forms-platform-utils.js +69 -0
  153. package/dist/native/forms-platform.js +429 -0
  154. package/dist/native/index.js +1879 -0
  155. package/dist/native/local-offline.js +81 -0
  156. package/dist/native/observability.js +182 -0
  157. package/dist/native/reachability.js +28 -0
  158. package/dist/native/redaction.js +44 -0
  159. package/dist/native/storage/async-storage.native.js +91 -0
  160. package/dist/native/storage/keyed-store.js +74 -0
  161. package/dist/native/storage/memory.js +26 -0
  162. package/dist/native/storage/port.js +16 -0
  163. package/dist/native/transport.js +27 -0
  164. package/dist/node/approval.js +1 -0
  165. package/dist/node/collaboration.js +0 -0
  166. package/dist/node/contracts-runtime-core.feature.js +1 -0
  167. package/dist/node/engine/backoff.js +1 -0
  168. package/dist/node/engine/cache-store.js +1 -0
  169. package/dist/node/engine/conflict-registry.js +1 -0
  170. package/dist/node/engine/conflict-resolution.js +1 -0
  171. package/dist/node/engine/data-engine-host.js +1 -0
  172. package/dist/node/engine/data-engine-test-helpers.js +1 -0
  173. package/dist/node/engine/data-engine.js +1 -0
  174. package/dist/node/engine/dedup.js +1 -0
  175. package/dist/node/engine/events.js +1 -0
  176. package/dist/node/engine/index.js +1 -0
  177. package/dist/node/engine/maintenance.js +1 -0
  178. package/dist/node/engine/mutation-engine.js +1 -0
  179. package/dist/node/engine/mutation-queue.js +1 -0
  180. package/dist/node/engine/mutation-types.js +0 -0
  181. package/dist/node/engine/no-cache-store.js +1 -0
  182. package/dist/node/engine/online-only-data-engine.js +1 -0
  183. package/dist/node/engine/online-only-mutation-engine.js +1 -0
  184. package/dist/node/engine/query-engine.js +1 -0
  185. package/dist/node/engine/state.js +1 -0
  186. package/dist/node/engine/types.js +0 -0
  187. package/dist/node/forms-platform-csv-columns.js +1 -0
  188. package/dist/node/forms-platform-csv.js +2 -0
  189. package/dist/node/forms-platform-management.js +1 -0
  190. package/dist/node/forms-platform-navigation.js +1 -0
  191. package/dist/node/forms-platform-submission.js +1 -0
  192. package/dist/node/forms-platform-types.js +1 -0
  193. package/dist/node/forms-platform-utils.js +1 -0
  194. package/dist/node/forms-platform.js +2 -0
  195. package/dist/node/index.js +3 -0
  196. package/dist/node/local-offline.js +1 -0
  197. package/dist/node/observability.js +1 -0
  198. package/dist/node/reachability.js +1 -0
  199. package/dist/node/redaction.js +1 -0
  200. package/dist/node/storage/keyed-store.js +1 -0
  201. package/dist/node/storage/memory.js +1 -0
  202. package/dist/node/storage/port.js +1 -0
  203. package/dist/node/transport.js +2 -0
  204. package/dist/observability.d.ts +34 -0
  205. package/dist/observability.js +2 -0
  206. package/dist/observability.test.d.ts +1 -0
  207. package/dist/reachability.d.ts +63 -0
  208. package/dist/reachability.js +2 -0
  209. package/dist/reachability.test.d.ts +1 -0
  210. package/dist/redaction.d.ts +34 -0
  211. package/dist/redaction.js +2 -0
  212. package/dist/redaction.test.d.ts +1 -0
  213. package/dist/storage/async-storage.native.d.ts +23 -0
  214. package/dist/storage/async-storage.native.test.d.ts +1 -0
  215. package/dist/storage/indexeddb.web.d.ts +13 -0
  216. package/dist/storage/indexeddb.web.js +2 -0
  217. package/dist/storage/keyed-store.d.ts +25 -0
  218. package/dist/storage/keyed-store.js +2 -0
  219. package/dist/storage/keyed-store.test.d.ts +1 -0
  220. package/dist/storage/memory.d.ts +11 -0
  221. package/dist/storage/memory.js +2 -0
  222. package/dist/storage/memory.test.d.ts +1 -0
  223. package/dist/storage/port.d.ts +34 -0
  224. package/dist/storage/port.js +2 -0
  225. package/dist/transport.d.ts +53 -0
  226. package/dist/transport.js +3 -0
  227. package/package.json +219 -0
@@ -0,0 +1,360 @@
1
+ // src/engine/query-engine.ts
2
+ import { createQueryKey } from "@lssm/lib.contracts-spec/query/key";
3
+
4
+ // src/reachability.ts
5
+ function isNetworkAllowed(signal) {
6
+ return signal !== "offline";
7
+ }
8
+ function createStaticReachabilitySource(signal = "online") {
9
+ const snapshot = { signal, since: Date.now() };
10
+ return {
11
+ getSnapshot: () => snapshot,
12
+ subscribe: () => () => {}
13
+ };
14
+ }
15
+ var reachabilityPortDoc = {
16
+ id: "docs.contracts-runtime-core.reachability",
17
+ title: "Reachability input port (tri-state)",
18
+ summary: "Engine-owned reachability input port; integrations feed it, the engine never imports them.",
19
+ kind: "reference",
20
+ visibility: "internal",
21
+ route: "/docs/contracts-runtime-core/reachability",
22
+ domain: "contracts-runtime-core",
23
+ owners: ["@contractspec-core"],
24
+ tags: ["contracts-runtime-core", "reachability", "offline", "port"],
25
+ body: "Tri-state OnlineSignal ('online' | 'restricted' | 'offline'). 'online' allows normal fetch + queue drain. 'restricted' is online-degraded: the engine fetches and drains the queue as if online but may mark results stale-eligible; it never suspends network nor activates the offline queue. Only 'offline' suspends outbound network and activates the durable offline mutation queue. The engine reads reachability solely through ReachabilitySource and MUST NOT import integrations/* — concrete sources (runtime-local networkReachability) are wired via an integration→port adapter (plan §2.3/§4.2, Architect finding 1). isNetworkAllowed mirrors runtime-local's outboundNetworkAllowed = reachability !== 'offline'. createStaticReachabilitySource('online') is the B1a default and online test fixture."
26
+ };
27
+
28
+ // src/storage/memory.ts
29
+ function createMemoryStorage() {
30
+ const store = new Map;
31
+ return {
32
+ get: async (key) => {
33
+ const value = store.get(key);
34
+ return value === undefined ? undefined : structuredClone(value);
35
+ },
36
+ set: async (key, value) => {
37
+ store.set(key, structuredClone(value));
38
+ },
39
+ remove: async (key) => {
40
+ store.delete(key);
41
+ },
42
+ keys: async (prefix) => {
43
+ const all = [...store.keys()];
44
+ return prefix === undefined ? all : all.filter((key) => key.startsWith(prefix));
45
+ },
46
+ clear: async () => {
47
+ store.clear();
48
+ }
49
+ };
50
+ }
51
+
52
+ // src/engine/cache-store.ts
53
+ import { invalidationTagMatches } from "@lssm/lib.contracts-spec/query/invalidation";
54
+ var CACHE_PREFIX = "cache:";
55
+ function createCacheStore(storage) {
56
+ const tagIndex = new Map;
57
+ const storageKey = (key) => `${CACHE_PREFIX}${key}`;
58
+ return {
59
+ read: (key) => storage.get(storageKey(key)),
60
+ write: async (entry) => {
61
+ tagIndex.set(entry.key, entry.tags);
62
+ await storage.set(storageKey(entry.key), entry);
63
+ },
64
+ remove: async (key) => {
65
+ tagIndex.delete(key);
66
+ await storage.remove(storageKey(key));
67
+ },
68
+ entries: async () => {
69
+ const keys = await storage.keys(CACHE_PREFIX);
70
+ const all = [];
71
+ for (const key of keys) {
72
+ const entry = await storage.get(key);
73
+ if (entry !== undefined)
74
+ all.push(entry);
75
+ }
76
+ return all;
77
+ },
78
+ invalidate: async (tags, now) => {
79
+ const affected = [];
80
+ for (const [cacheKey, entryTags] of tagIndex.entries()) {
81
+ const matched = entryTags.some((subject) => tags.some((pattern) => invalidationTagMatches(subject, pattern)));
82
+ if (!matched)
83
+ continue;
84
+ const stored = await storage.get(storageKey(cacheKey));
85
+ if (stored === undefined)
86
+ continue;
87
+ await storage.set(storageKey(cacheKey), {
88
+ ...stored,
89
+ status: "stale",
90
+ staleAt: now
91
+ });
92
+ affected.push(cacheKey);
93
+ }
94
+ return affected;
95
+ }
96
+ };
97
+ }
98
+
99
+ // src/engine/dedup.ts
100
+ function createInflightRegistry() {
101
+ const inflight = new Map;
102
+ return {
103
+ run(key, factory) {
104
+ const existing = inflight.get(key);
105
+ if (existing !== undefined)
106
+ return existing;
107
+ const promise = factory().finally(() => {
108
+ inflight.delete(key);
109
+ });
110
+ inflight.set(key, promise);
111
+ return promise;
112
+ },
113
+ size() {
114
+ return inflight.size;
115
+ }
116
+ };
117
+ }
118
+
119
+ // src/engine/events.ts
120
+ function createEngineEvents() {
121
+ const listeners = new Set;
122
+ return {
123
+ emit(event) {
124
+ for (const listener of listeners)
125
+ listener(event);
126
+ },
127
+ subscribe(listener) {
128
+ listeners.add(listener);
129
+ return () => {
130
+ listeners.delete(listener);
131
+ };
132
+ }
133
+ };
134
+ }
135
+
136
+ // src/engine/state.ts
137
+ function computeIsStale(entry, now) {
138
+ if (entry?.staleAt === undefined)
139
+ return false;
140
+ return now >= entry.staleAt;
141
+ }
142
+ function idleState() {
143
+ return {
144
+ status: "idle",
145
+ fetchStatus: "idle",
146
+ cacheStatus: "empty",
147
+ isStale: false,
148
+ isOffline: false
149
+ };
150
+ }
151
+ function loadingState(entry, now) {
152
+ if (entry === undefined) {
153
+ return {
154
+ status: "loading",
155
+ fetchStatus: "fetching",
156
+ cacheStatus: "empty",
157
+ isStale: false,
158
+ isOffline: false
159
+ };
160
+ }
161
+ const isStale = computeIsStale(entry, now);
162
+ return {
163
+ status: "success",
164
+ fetchStatus: "fetching",
165
+ data: entry.data,
166
+ cacheStatus: isStale ? "stale" : entry.status,
167
+ isStale,
168
+ isOffline: false,
169
+ versionToken: entry.versionToken,
170
+ dataUpdatedAt: entry.storedAt,
171
+ ...entry.pageInfo === undefined ? {} : { pageInfo: entry.pageInfo }
172
+ };
173
+ }
174
+ function successState(entry, now) {
175
+ const isStale = computeIsStale(entry, now);
176
+ return {
177
+ status: "success",
178
+ fetchStatus: "idle",
179
+ data: entry.data,
180
+ cacheStatus: isStale ? "stale" : entry.status,
181
+ isStale,
182
+ isOffline: false,
183
+ versionToken: entry.versionToken,
184
+ dataUpdatedAt: entry.storedAt,
185
+ ...entry.pageInfo === undefined ? {} : { pageInfo: entry.pageInfo }
186
+ };
187
+ }
188
+ function errorState(problem, entry, now) {
189
+ return {
190
+ status: "error",
191
+ fetchStatus: "idle",
192
+ data: entry?.data,
193
+ error: problem,
194
+ cacheStatus: entry === undefined ? "empty" : entry.status,
195
+ isStale: computeIsStale(entry, now),
196
+ isOffline: false,
197
+ versionToken: entry?.versionToken,
198
+ dataUpdatedAt: entry?.storedAt,
199
+ errorUpdatedAt: now
200
+ };
201
+ }
202
+ function authExpiredState(problem, entry, now) {
203
+ return {
204
+ status: "auth-expired",
205
+ fetchStatus: "idle",
206
+ data: entry?.data,
207
+ error: problem,
208
+ cacheStatus: entry === undefined ? "empty" : entry.status,
209
+ isStale: computeIsStale(entry, now),
210
+ isOffline: false,
211
+ versionToken: entry?.versionToken,
212
+ dataUpdatedAt: entry?.storedAt,
213
+ errorUpdatedAt: now
214
+ };
215
+ }
216
+ function conflictState(entry, conflict, now) {
217
+ return {
218
+ status: entry === undefined ? "idle" : "success",
219
+ fetchStatus: "idle",
220
+ data: entry?.data,
221
+ cacheStatus: "conflict",
222
+ isStale: computeIsStale(entry, now),
223
+ isOffline: false,
224
+ conflict,
225
+ versionToken: conflict.versionToken ?? entry?.versionToken,
226
+ dataUpdatedAt: entry?.storedAt
227
+ };
228
+ }
229
+ function offlineState(entry, now) {
230
+ return {
231
+ status: entry === undefined ? "idle" : "success",
232
+ fetchStatus: "idle",
233
+ data: entry?.data,
234
+ cacheStatus: "offline",
235
+ isStale: computeIsStale(entry, now),
236
+ isOffline: true,
237
+ versionToken: entry?.versionToken,
238
+ dataUpdatedAt: entry?.storedAt,
239
+ ...entry?.pageInfo === undefined ? {} : { pageInfo: entry.pageInfo }
240
+ };
241
+ }
242
+
243
+ // src/engine/query-engine.ts
244
+ var DEFAULT_STALE_TIME_MS = 0;
245
+ function createQueryEngine(options) {
246
+ const { transport } = options;
247
+ const storage = options.storage ?? createMemoryStorage();
248
+ const reachability = options.reachability ?? createStaticReachabilitySource("online");
249
+ const now = options.now ?? (() => Date.now());
250
+ const defaultStaleTimeMs = options.defaultStaleTimeMs ?? DEFAULT_STALE_TIME_MS;
251
+ const cache = options.cacheStore ?? createCacheStore(storage);
252
+ const events = options.events ?? createEngineEvents();
253
+ const inflight = createInflightRegistry();
254
+ const states = new Map;
255
+ const listeners = new Map;
256
+ function emit(key, state) {
257
+ states.set(key, state);
258
+ const set = listeners.get(key);
259
+ if (set === undefined)
260
+ return;
261
+ for (const listener of set)
262
+ listener(state);
263
+ }
264
+ async function query(envelope, ctx) {
265
+ const key = createQueryKey(envelope);
266
+ const signal = reachability.getSnapshot().signal;
267
+ const entry = await cache.read(key);
268
+ if (!isNetworkAllowed(signal)) {
269
+ const state = offlineState(entry, now());
270
+ emit(key, state);
271
+ return state;
272
+ }
273
+ if (entry !== undefined && !computeIsStale(entry, now())) {
274
+ events.emit({ type: "cache-hit", key });
275
+ const state = successState(entry, now());
276
+ emit(key, state);
277
+ return state;
278
+ }
279
+ events.emit({ type: "cache-miss", key });
280
+ emit(key, loadingState(entry, now()));
281
+ const startedAt = now();
282
+ const result = await inflight.run(key, () => transport.execute(envelope, ctx));
283
+ const settledAt = now();
284
+ if (!result.ok) {
285
+ events.emit({
286
+ type: "fetch-error",
287
+ key,
288
+ code: result.code,
289
+ latencyMs: settledAt - startedAt
290
+ });
291
+ const state = result.problem.category === "auth" ? authExpiredState(result.problem, entry, settledAt) : errorState(result.problem, entry, settledAt);
292
+ emit(key, state);
293
+ return state;
294
+ }
295
+ events.emit({
296
+ type: "fetch-success",
297
+ key,
298
+ latencyMs: settledAt - startedAt
299
+ });
300
+ const staleTimeMs = envelope.cache?.staleTimeMs ?? defaultStaleTimeMs;
301
+ const cacheTimeMs = envelope.cache?.cacheTimeMs;
302
+ const written = {
303
+ key,
304
+ data: result.data.data,
305
+ versionToken: result.data.versionToken ?? result.versionToken,
306
+ status: signal === "restricted" ? "warm" : "fresh",
307
+ tags: envelope.tags ?? [],
308
+ storedAt: settledAt,
309
+ staleAt: settledAt + staleTimeMs,
310
+ envelope,
311
+ ...cacheTimeMs === undefined ? {} : { expiresAt: settledAt + cacheTimeMs },
312
+ ...result.data.pageInfo === undefined ? {} : { pageInfo: result.data.pageInfo }
313
+ };
314
+ await cache.write(written);
315
+ const state = successState(written, settledAt);
316
+ emit(key, state);
317
+ return state;
318
+ }
319
+ async function refresh(key) {
320
+ const entry = await cache.read(key);
321
+ const state = entry === undefined ? idleState() : successState(entry, now());
322
+ emit(key, state);
323
+ return state;
324
+ }
325
+ async function emitConflict(key, conflict) {
326
+ const entry = await cache.read(key);
327
+ const state = conflictState(entry, conflict, now());
328
+ emit(key, state);
329
+ return state;
330
+ }
331
+ function getState(envelope) {
332
+ const existing = states.get(createQueryKey(envelope));
333
+ return existing ?? idleState();
334
+ }
335
+ function subscribe(envelope, listener) {
336
+ const key = createQueryKey(envelope);
337
+ let set = listeners.get(key);
338
+ if (set === undefined) {
339
+ set = new Set;
340
+ listeners.set(key, set);
341
+ }
342
+ set.add(listener);
343
+ return () => {
344
+ listeners.get(key)?.delete(listener);
345
+ };
346
+ }
347
+ async function invalidateTags(tags) {
348
+ const affected = await cache.invalidate(tags, now());
349
+ for (const key of affected) {
350
+ const entry = await cache.read(key);
351
+ if (entry !== undefined)
352
+ emit(key, successState(entry, now()));
353
+ }
354
+ return affected;
355
+ }
356
+ return { query, getState, subscribe, invalidateTags, refresh, emitConflict };
357
+ }
358
+ export {
359
+ createQueryEngine
360
+ };
@@ -0,0 +1,116 @@
1
+ // src/engine/state.ts
2
+ function computeIsStale(entry, now) {
3
+ if (entry?.staleAt === undefined)
4
+ return false;
5
+ return now >= entry.staleAt;
6
+ }
7
+ function idleState() {
8
+ return {
9
+ status: "idle",
10
+ fetchStatus: "idle",
11
+ cacheStatus: "empty",
12
+ isStale: false,
13
+ isOffline: false
14
+ };
15
+ }
16
+ function loadingState(entry, now) {
17
+ if (entry === undefined) {
18
+ return {
19
+ status: "loading",
20
+ fetchStatus: "fetching",
21
+ cacheStatus: "empty",
22
+ isStale: false,
23
+ isOffline: false
24
+ };
25
+ }
26
+ const isStale = computeIsStale(entry, now);
27
+ return {
28
+ status: "success",
29
+ fetchStatus: "fetching",
30
+ data: entry.data,
31
+ cacheStatus: isStale ? "stale" : entry.status,
32
+ isStale,
33
+ isOffline: false,
34
+ versionToken: entry.versionToken,
35
+ dataUpdatedAt: entry.storedAt,
36
+ ...entry.pageInfo === undefined ? {} : { pageInfo: entry.pageInfo }
37
+ };
38
+ }
39
+ function successState(entry, now) {
40
+ const isStale = computeIsStale(entry, now);
41
+ return {
42
+ status: "success",
43
+ fetchStatus: "idle",
44
+ data: entry.data,
45
+ cacheStatus: isStale ? "stale" : entry.status,
46
+ isStale,
47
+ isOffline: false,
48
+ versionToken: entry.versionToken,
49
+ dataUpdatedAt: entry.storedAt,
50
+ ...entry.pageInfo === undefined ? {} : { pageInfo: entry.pageInfo }
51
+ };
52
+ }
53
+ function errorState(problem, entry, now) {
54
+ return {
55
+ status: "error",
56
+ fetchStatus: "idle",
57
+ data: entry?.data,
58
+ error: problem,
59
+ cacheStatus: entry === undefined ? "empty" : entry.status,
60
+ isStale: computeIsStale(entry, now),
61
+ isOffline: false,
62
+ versionToken: entry?.versionToken,
63
+ dataUpdatedAt: entry?.storedAt,
64
+ errorUpdatedAt: now
65
+ };
66
+ }
67
+ function authExpiredState(problem, entry, now) {
68
+ return {
69
+ status: "auth-expired",
70
+ fetchStatus: "idle",
71
+ data: entry?.data,
72
+ error: problem,
73
+ cacheStatus: entry === undefined ? "empty" : entry.status,
74
+ isStale: computeIsStale(entry, now),
75
+ isOffline: false,
76
+ versionToken: entry?.versionToken,
77
+ dataUpdatedAt: entry?.storedAt,
78
+ errorUpdatedAt: now
79
+ };
80
+ }
81
+ function conflictState(entry, conflict, now) {
82
+ return {
83
+ status: entry === undefined ? "idle" : "success",
84
+ fetchStatus: "idle",
85
+ data: entry?.data,
86
+ cacheStatus: "conflict",
87
+ isStale: computeIsStale(entry, now),
88
+ isOffline: false,
89
+ conflict,
90
+ versionToken: conflict.versionToken ?? entry?.versionToken,
91
+ dataUpdatedAt: entry?.storedAt
92
+ };
93
+ }
94
+ function offlineState(entry, now) {
95
+ return {
96
+ status: entry === undefined ? "idle" : "success",
97
+ fetchStatus: "idle",
98
+ data: entry?.data,
99
+ cacheStatus: "offline",
100
+ isStale: computeIsStale(entry, now),
101
+ isOffline: true,
102
+ versionToken: entry?.versionToken,
103
+ dataUpdatedAt: entry?.storedAt,
104
+ ...entry?.pageInfo === undefined ? {} : { pageInfo: entry.pageInfo }
105
+ };
106
+ }
107
+ export {
108
+ authExpiredState,
109
+ computeIsStale,
110
+ conflictState,
111
+ errorState,
112
+ idleState,
113
+ loadingState,
114
+ offlineState,
115
+ successState
116
+ };
File without changes
@@ -0,0 +1,31 @@
1
+ // src/forms-platform-csv-columns.ts
2
+ import { flattenPortableFormFields } from "@lssm/lib.contracts-spec/forms";
3
+ function formCsvColumns(document) {
4
+ const questionNames = new Set(document.questions.map(({ name }) => name));
5
+ const questions = document.questions.map(questionColumn);
6
+ const portableSensitive = new Set(document.formSpec?.policy?.pii ?? []);
7
+ const allFields = flattenPortableFormFields(document.formSpec?.fields ?? []);
8
+ const portable = Object.entries(document.formSpec?.modelFields ?? {}).flatMap(([name, modelField]) => {
9
+ if (questionNames.has(name))
10
+ return [];
11
+ const field = allFields.find((candidate) => candidate.name === name);
12
+ return [
13
+ {
14
+ id: name,
15
+ labelKey: field?.labelI18n ?? name,
16
+ sensitive: portableSensitive.has(name) || [...portableSensitive].some((path) => path.startsWith(`${name}.`)) || field?.kind === "email" || field?.kind === "phone" || modelField.type === "email" || modelField.type === "phone"
17
+ }
18
+ ];
19
+ });
20
+ return [...questions, ...portable];
21
+ }
22
+ function questionColumn(question) {
23
+ return {
24
+ id: question.id,
25
+ labelKey: question.labelKey,
26
+ sensitive: question.sensitive === true || question.kind === "email" || question.kind === "phone"
27
+ };
28
+ }
29
+ export {
30
+ formCsvColumns
31
+ };
@@ -0,0 +1,150 @@
1
+ // src/forms-platform-csv-columns.ts
2
+ import { flattenPortableFormFields } from "@lssm/lib.contracts-spec/forms";
3
+ function formCsvColumns(document) {
4
+ const questionNames = new Set(document.questions.map(({ name }) => name));
5
+ const questions = document.questions.map(questionColumn);
6
+ const portableSensitive = new Set(document.formSpec?.policy?.pii ?? []);
7
+ const allFields = flattenPortableFormFields(document.formSpec?.fields ?? []);
8
+ const portable = Object.entries(document.formSpec?.modelFields ?? {}).flatMap(([name, modelField]) => {
9
+ if (questionNames.has(name))
10
+ return [];
11
+ const field = allFields.find((candidate) => candidate.name === name);
12
+ return [
13
+ {
14
+ id: name,
15
+ labelKey: field?.labelI18n ?? name,
16
+ sensitive: portableSensitive.has(name) || [...portableSensitive].some((path) => path.startsWith(`${name}.`)) || field?.kind === "email" || field?.kind === "phone" || modelField.type === "email" || modelField.type === "phone"
17
+ }
18
+ ];
19
+ });
20
+ return [...questions, ...portable];
21
+ }
22
+ function questionColumn(question) {
23
+ return {
24
+ id: question.id,
25
+ labelKey: question.labelKey,
26
+ sensitive: question.sensitive === true || question.kind === "email" || question.kind === "phone"
27
+ };
28
+ }
29
+
30
+ // src/forms-platform-utils.ts
31
+ import {
32
+ compileFormDocument,
33
+ validateFormDocument
34
+ } from "@lssm/lib.contracts-spec/forms";
35
+
36
+ // src/forms-platform-types.ts
37
+ class FormsPlatformError extends Error {
38
+ code;
39
+ constructor(code, message) {
40
+ super(message);
41
+ this.code = code;
42
+ this.name = "FormsPlatformError";
43
+ }
44
+ }
45
+
46
+ // src/forms-platform-utils.ts
47
+ function requireFormDefinition(definition, scope) {
48
+ const matches = definition?.owner.product === scope.product && definition.owner.tenantRef === scope.tenantRef && definition.owner.workspaceRef === scope.workspaceRef && definition.owner.ownerRef === scope.ownerRef;
49
+ if (!definition || !matches) {
50
+ throw new FormsPlatformError("FORM_NOT_FOUND", "Form definition was not found in the caller scope");
51
+ }
52
+ return definition;
53
+ }
54
+ function validateRuntimeFormDocument(document) {
55
+ const issues = validateFormDocument(document);
56
+ if (issues.length) {
57
+ throw new FormsPlatformError("INVALID_FORM_DOCUMENT", issues.map((issue) => issue.message).join("; "));
58
+ }
59
+ compileFormDocument(document);
60
+ }
61
+ function formAssuranceMeetsRequirement(assurance, requirement) {
62
+ if (requirement === "none")
63
+ return true;
64
+ if (requirement === "either")
65
+ return assurance !== "anonymous";
66
+ if (requirement === "both")
67
+ return assurance === "both";
68
+ return assurance === requirement || assurance === "both";
69
+ }
70
+ function resolveFormResponseStatus(document, assurance) {
71
+ if (formAssuranceMeetsRequirement(assurance, document.verification.requirement)) {
72
+ return "completed";
73
+ }
74
+ return document.verification.unverifiedDisposition === "pending-hidden" ? "pending-verification" : "completed-unverified";
75
+ }
76
+ function resolveFormLocale(document, candidates) {
77
+ const supported = new Set(document.content.enabledLocales.map((locale) => locale.toLowerCase()));
78
+ for (const candidate of candidates) {
79
+ if (!candidate)
80
+ continue;
81
+ const normalized = candidate.trim().toLowerCase();
82
+ const exact = document.content.enabledLocales.find((locale) => locale.toLowerCase() === normalized);
83
+ if (supported.has(normalized) && exact)
84
+ return exact;
85
+ const language = normalized.split("-")[0];
86
+ const match = document.content.enabledLocales.find((locale) => locale.toLowerCase().split("-")[0] === language);
87
+ if (match)
88
+ return match;
89
+ }
90
+ return document.content.defaultLocale;
91
+ }
92
+
93
+ // src/forms-platform-csv.ts
94
+ var RESPONSE_EXTENSIONS_COLUMN = "response_extensions:AI follow-ups";
95
+ function csvCell(value) {
96
+ const raw = Array.isArray(value) ? value.every((item) => typeof item !== "object") ? value.join(";") : JSON.stringify(value) : value == null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
97
+ const protectedValue = /^[\s]*[=+\-@]/.test(raw) ? `'${raw}` : raw;
98
+ return `"${protectedValue.replaceAll('"', '""')}"`;
99
+ }
100
+ function csvRow(values) {
101
+ return `${values.map(csvCell).join(",")}\r
102
+ `;
103
+ }
104
+ function responseRow(revision, response, options) {
105
+ if (response.revisionId !== revision.id) {
106
+ throw new Error("CSV responses must belong to the exported revision");
107
+ }
108
+ const columns = formCsvColumns(revision.document);
109
+ const baseIds = new Set(columns.map(({ id }) => id));
110
+ const extensions = Object.fromEntries(Object.entries(response.answers).filter(([id]) => !baseIds.has(id)));
111
+ return [
112
+ response.id,
113
+ response.status,
114
+ response.locale,
115
+ response.submittedAt ?? "",
116
+ ...columns.map((column) => {
117
+ const mayIncludeSensitive = options.includeSensitive && options.sensitiveGrant?.authorized;
118
+ if (!mayIncludeSensitive && column.sensitive) {
119
+ return "[REDACTED]";
120
+ }
121
+ return response.answers[column.id];
122
+ }),
123
+ options.includeSensitive && options.sensitiveGrant?.authorized ? extensions : "[REDACTED]"
124
+ ];
125
+ }
126
+ function csvHeaders(revision, locale) {
127
+ const resolved = resolveFormLocale(revision.document, [locale]);
128
+ const catalog = revision.document.content.catalogs[resolved] ?? {};
129
+ return [
130
+ "response_id",
131
+ "status",
132
+ "locale",
133
+ "submitted_at",
134
+ ...formCsvColumns(revision.document).map((column) => `${column.id}:${catalog[column.labelKey] ?? column.labelKey}`),
135
+ RESPONSE_EXTENSIONS_COLUMN
136
+ ];
137
+ }
138
+ function serializeFormResponsesCsv(revision, responses, options = {}) {
139
+ return `\uFEFF${csvRow(csvHeaders(revision, options.locale))}${responses.map((response) => csvRow(responseRow(revision, response, options))).join("")}`;
140
+ }
141
+ async function* streamFormResponsesCsv(revision, responses, options = {}) {
142
+ yield `\uFEFF${csvRow(csvHeaders(revision, options.locale))}`;
143
+ for await (const response of responses) {
144
+ yield csvRow(responseRow(revision, response, options));
145
+ }
146
+ }
147
+ export {
148
+ serializeFormResponsesCsv,
149
+ streamFormResponsesCsv
150
+ };