@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.
- package/README.md +110 -0
- package/dist/approval.d.ts +13 -0
- package/dist/approval.js +2 -0
- package/dist/approval.test.d.ts +1 -0
- package/dist/browser/approval.js +1 -0
- package/dist/browser/collaboration.js +0 -0
- package/dist/browser/contracts-runtime-core.feature.js +1 -0
- package/dist/browser/engine/backoff.js +1 -0
- package/dist/browser/engine/cache-store.js +1 -0
- package/dist/browser/engine/conflict-registry.js +1 -0
- package/dist/browser/engine/conflict-resolution.js +1 -0
- package/dist/browser/engine/data-engine-host.js +1 -0
- package/dist/browser/engine/data-engine-test-helpers.js +1 -0
- package/dist/browser/engine/data-engine.js +1 -0
- package/dist/browser/engine/dedup.js +1 -0
- package/dist/browser/engine/events.js +1 -0
- package/dist/browser/engine/index.js +1 -0
- package/dist/browser/engine/maintenance.js +1 -0
- package/dist/browser/engine/mutation-engine.js +1 -0
- package/dist/browser/engine/mutation-queue.js +1 -0
- package/dist/browser/engine/mutation-types.js +0 -0
- package/dist/browser/engine/no-cache-store.js +1 -0
- package/dist/browser/engine/online-only-data-engine.js +1 -0
- package/dist/browser/engine/online-only-mutation-engine.js +1 -0
- package/dist/browser/engine/query-engine.js +1 -0
- package/dist/browser/engine/state.js +1 -0
- package/dist/browser/engine/types.js +0 -0
- package/dist/browser/forms-platform-csv-columns.js +1 -0
- package/dist/browser/forms-platform-csv.js +2 -0
- package/dist/browser/forms-platform-management.js +1 -0
- package/dist/browser/forms-platform-navigation.js +1 -0
- package/dist/browser/forms-platform-submission.js +1 -0
- package/dist/browser/forms-platform-types.js +1 -0
- package/dist/browser/forms-platform-utils.js +1 -0
- package/dist/browser/forms-platform.js +2 -0
- package/dist/browser/index.js +3 -0
- package/dist/browser/local-offline.js +1 -0
- package/dist/browser/observability.js +1 -0
- package/dist/browser/reachability.js +1 -0
- package/dist/browser/redaction.js +1 -0
- package/dist/browser/storage/indexeddb.web.js +1 -0
- package/dist/browser/storage/keyed-store.js +1 -0
- package/dist/browser/storage/memory.js +1 -0
- package/dist/browser/storage/port.js +1 -0
- package/dist/browser/transport.js +2 -0
- package/dist/collaboration.d.ts +57 -0
- package/dist/collaboration.js +1 -0
- package/dist/contracts-runtime-core.feature.d.ts +1 -0
- package/dist/contracts-runtime-core.feature.js +2 -0
- package/dist/engine/backoff.d.ts +16 -0
- package/dist/engine/backoff.js +2 -0
- package/dist/engine/backoff.test.d.ts +1 -0
- package/dist/engine/cache-store.d.ts +19 -0
- package/dist/engine/cache-store.js +2 -0
- package/dist/engine/conflict-registry.d.ts +10 -0
- package/dist/engine/conflict-registry.js +2 -0
- package/dist/engine/conflict-registry.test.d.ts +1 -0
- package/dist/engine/conflict-resolution.d.ts +24 -0
- package/dist/engine/conflict-resolution.js +2 -0
- package/dist/engine/conflict-resolution.test.d.ts +1 -0
- package/dist/engine/data-engine-host.d.ts +30 -0
- package/dist/engine/data-engine-host.js +2 -0
- package/dist/engine/data-engine-local-offline.test.d.ts +1 -0
- package/dist/engine/data-engine-test-helpers.d.ts +30 -0
- package/dist/engine/data-engine-test-helpers.js +2 -0
- package/dist/engine/data-engine.d.ts +46 -0
- package/dist/engine/data-engine.js +2 -0
- package/dist/engine/data-engine.test.d.ts +1 -0
- package/dist/engine/dedup.d.ts +9 -0
- package/dist/engine/dedup.js +2 -0
- package/dist/engine/events.d.ts +76 -0
- package/dist/engine/events.economic-evidence.test.d.ts +1 -0
- package/dist/engine/events.js +2 -0
- package/dist/engine/index.d.ts +17 -0
- package/dist/engine/index.js +2 -0
- package/dist/engine/maintenance.d.ts +18 -0
- package/dist/engine/maintenance.js +2 -0
- package/dist/engine/maintenance.test.d.ts +1 -0
- package/dist/engine/mutation-engine.d.ts +2 -0
- package/dist/engine/mutation-engine.js +2 -0
- package/dist/engine/mutation-queue.d.ts +16 -0
- package/dist/engine/mutation-queue.js +2 -0
- package/dist/engine/mutation-queue.test.d.ts +1 -0
- package/dist/engine/mutation-types.d.ts +118 -0
- package/dist/engine/mutation-types.js +1 -0
- package/dist/engine/no-cache-store.d.ts +3 -0
- package/dist/engine/no-cache-store.js +2 -0
- package/dist/engine/online-only-data-engine.d.ts +9 -0
- package/dist/engine/online-only-data-engine.js +2 -0
- package/dist/engine/online-only-mutation-engine.d.ts +3 -0
- package/dist/engine/online-only-mutation-engine.js +2 -0
- package/dist/engine/query-engine.d.ts +7 -0
- package/dist/engine/query-engine.js +2 -0
- package/dist/engine/query-engine.test.d.ts +1 -0
- package/dist/engine/state.d.ts +37 -0
- package/dist/engine/state.js +2 -0
- package/dist/engine/types.d.ts +70 -0
- package/dist/engine/types.js +1 -0
- package/dist/forms-platform-choice.test.d.ts +1 -0
- package/dist/forms-platform-csv-columns.d.ts +7 -0
- package/dist/forms-platform-csv-columns.js +2 -0
- package/dist/forms-platform-csv.d.ts +12 -0
- package/dist/forms-platform-csv.js +3 -0
- package/dist/forms-platform-management.d.ts +12 -0
- package/dist/forms-platform-management.js +2 -0
- package/dist/forms-platform-navigation.d.ts +2 -0
- package/dist/forms-platform-navigation.js +2 -0
- package/dist/forms-platform-submission.d.ts +7 -0
- package/dist/forms-platform-submission.js +2 -0
- package/dist/forms-platform-types.d.ts +51 -0
- package/dist/forms-platform-types.js +2 -0
- package/dist/forms-platform-utils.d.ts +6 -0
- package/dist/forms-platform-utils.js +2 -0
- package/dist/forms-platform-utils.test.d.ts +1 -0
- package/dist/forms-platform.d.ts +19 -0
- package/dist/forms-platform.js +3 -0
- package/dist/forms-platform.test.d.ts +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +4 -0
- package/dist/local-offline.d.ts +17 -0
- package/dist/local-offline.js +2 -0
- package/dist/local-offline.test.d.ts +1 -0
- package/dist/native/approval.js +130 -0
- package/dist/native/collaboration.js +0 -0
- package/dist/native/contracts-runtime-core.feature.js +17 -0
- package/dist/native/engine/backoff.js +16 -0
- package/dist/native/engine/cache-store.js +49 -0
- package/dist/native/engine/conflict-registry.js +51 -0
- package/dist/native/engine/conflict-resolution.js +81 -0
- package/dist/native/engine/data-engine-host.js +1119 -0
- package/dist/native/engine/data-engine-test-helpers.js +117 -0
- package/dist/native/engine/data-engine.js +1106 -0
- package/dist/native/engine/dedup.js +22 -0
- package/dist/native/engine/events.js +19 -0
- package/dist/native/engine/index.js +1189 -0
- package/dist/native/engine/maintenance.js +36 -0
- package/dist/native/engine/mutation-engine.js +360 -0
- package/dist/native/engine/mutation-queue.js +26 -0
- package/dist/native/engine/mutation-types.js +0 -0
- package/dist/native/engine/no-cache-store.js +15 -0
- package/dist/native/engine/online-only-data-engine.js +422 -0
- package/dist/native/engine/online-only-mutation-engine.js +70 -0
- package/dist/native/engine/query-engine.js +360 -0
- package/dist/native/engine/state.js +116 -0
- package/dist/native/engine/types.js +0 -0
- package/dist/native/forms-platform-csv-columns.js +31 -0
- package/dist/native/forms-platform-csv.js +150 -0
- package/dist/native/forms-platform-management.js +179 -0
- package/dist/native/forms-platform-navigation.js +27 -0
- package/dist/native/forms-platform-submission.js +207 -0
- package/dist/native/forms-platform-types.js +12 -0
- package/dist/native/forms-platform-utils.js +69 -0
- package/dist/native/forms-platform.js +429 -0
- package/dist/native/index.js +1879 -0
- package/dist/native/local-offline.js +81 -0
- package/dist/native/observability.js +182 -0
- package/dist/native/reachability.js +28 -0
- package/dist/native/redaction.js +44 -0
- package/dist/native/storage/async-storage.native.js +91 -0
- package/dist/native/storage/keyed-store.js +74 -0
- package/dist/native/storage/memory.js +26 -0
- package/dist/native/storage/port.js +16 -0
- package/dist/native/transport.js +27 -0
- package/dist/node/approval.js +1 -0
- package/dist/node/collaboration.js +0 -0
- package/dist/node/contracts-runtime-core.feature.js +1 -0
- package/dist/node/engine/backoff.js +1 -0
- package/dist/node/engine/cache-store.js +1 -0
- package/dist/node/engine/conflict-registry.js +1 -0
- package/dist/node/engine/conflict-resolution.js +1 -0
- package/dist/node/engine/data-engine-host.js +1 -0
- package/dist/node/engine/data-engine-test-helpers.js +1 -0
- package/dist/node/engine/data-engine.js +1 -0
- package/dist/node/engine/dedup.js +1 -0
- package/dist/node/engine/events.js +1 -0
- package/dist/node/engine/index.js +1 -0
- package/dist/node/engine/maintenance.js +1 -0
- package/dist/node/engine/mutation-engine.js +1 -0
- package/dist/node/engine/mutation-queue.js +1 -0
- package/dist/node/engine/mutation-types.js +0 -0
- package/dist/node/engine/no-cache-store.js +1 -0
- package/dist/node/engine/online-only-data-engine.js +1 -0
- package/dist/node/engine/online-only-mutation-engine.js +1 -0
- package/dist/node/engine/query-engine.js +1 -0
- package/dist/node/engine/state.js +1 -0
- package/dist/node/engine/types.js +0 -0
- package/dist/node/forms-platform-csv-columns.js +1 -0
- package/dist/node/forms-platform-csv.js +2 -0
- package/dist/node/forms-platform-management.js +1 -0
- package/dist/node/forms-platform-navigation.js +1 -0
- package/dist/node/forms-platform-submission.js +1 -0
- package/dist/node/forms-platform-types.js +1 -0
- package/dist/node/forms-platform-utils.js +1 -0
- package/dist/node/forms-platform.js +2 -0
- package/dist/node/index.js +3 -0
- package/dist/node/local-offline.js +1 -0
- package/dist/node/observability.js +1 -0
- package/dist/node/reachability.js +1 -0
- package/dist/node/redaction.js +1 -0
- package/dist/node/storage/keyed-store.js +1 -0
- package/dist/node/storage/memory.js +1 -0
- package/dist/node/storage/port.js +1 -0
- package/dist/node/transport.js +2 -0
- package/dist/observability.d.ts +34 -0
- package/dist/observability.js +2 -0
- package/dist/observability.test.d.ts +1 -0
- package/dist/reachability.d.ts +63 -0
- package/dist/reachability.js +2 -0
- package/dist/reachability.test.d.ts +1 -0
- package/dist/redaction.d.ts +34 -0
- package/dist/redaction.js +2 -0
- package/dist/redaction.test.d.ts +1 -0
- package/dist/storage/async-storage.native.d.ts +23 -0
- package/dist/storage/async-storage.native.test.d.ts +1 -0
- package/dist/storage/indexeddb.web.d.ts +13 -0
- package/dist/storage/indexeddb.web.js +2 -0
- package/dist/storage/keyed-store.d.ts +25 -0
- package/dist/storage/keyed-store.js +2 -0
- package/dist/storage/keyed-store.test.d.ts +1 -0
- package/dist/storage/memory.d.ts +11 -0
- package/dist/storage/memory.js +2 -0
- package/dist/storage/memory.test.d.ts +1 -0
- package/dist/storage/port.d.ts +34 -0
- package/dist/storage/port.js +2 -0
- package/dist/transport.d.ts +53 -0
- package/dist/transport.js +3 -0
- package/package.json +219 -0
|
@@ -0,0 +1,1189 @@
|
|
|
1
|
+
// src/engine/backoff.ts
|
|
2
|
+
var DEFAULT_BASE_MS = 200;
|
|
3
|
+
var DEFAULT_FACTOR = 2;
|
|
4
|
+
var DEFAULT_MAX_MS = 30000;
|
|
5
|
+
function backoffDelayMs(attempt, options = {}) {
|
|
6
|
+
const base = options.baseMs ?? DEFAULT_BASE_MS;
|
|
7
|
+
const factor = options.factor ?? DEFAULT_FACTOR;
|
|
8
|
+
const max = options.maxMs ?? DEFAULT_MAX_MS;
|
|
9
|
+
const jitter = options.jitter ?? Math.random;
|
|
10
|
+
const exponent = Math.max(0, attempt - 1);
|
|
11
|
+
const raw = Math.min(max, base * factor ** exponent);
|
|
12
|
+
return Math.round(raw * (0.5 + jitter() * 0.5));
|
|
13
|
+
}
|
|
14
|
+
// src/engine/cache-store.ts
|
|
15
|
+
import { invalidationTagMatches } from "@lssm/lib.contracts-spec/query/invalidation";
|
|
16
|
+
var CACHE_PREFIX = "cache:";
|
|
17
|
+
function createCacheStore(storage) {
|
|
18
|
+
const tagIndex = new Map;
|
|
19
|
+
const storageKey = (key) => `${CACHE_PREFIX}${key}`;
|
|
20
|
+
return {
|
|
21
|
+
read: (key) => storage.get(storageKey(key)),
|
|
22
|
+
write: async (entry) => {
|
|
23
|
+
tagIndex.set(entry.key, entry.tags);
|
|
24
|
+
await storage.set(storageKey(entry.key), entry);
|
|
25
|
+
},
|
|
26
|
+
remove: async (key) => {
|
|
27
|
+
tagIndex.delete(key);
|
|
28
|
+
await storage.remove(storageKey(key));
|
|
29
|
+
},
|
|
30
|
+
entries: async () => {
|
|
31
|
+
const keys = await storage.keys(CACHE_PREFIX);
|
|
32
|
+
const all = [];
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
const entry = await storage.get(key);
|
|
35
|
+
if (entry !== undefined)
|
|
36
|
+
all.push(entry);
|
|
37
|
+
}
|
|
38
|
+
return all;
|
|
39
|
+
},
|
|
40
|
+
invalidate: async (tags, now) => {
|
|
41
|
+
const affected = [];
|
|
42
|
+
for (const [cacheKey, entryTags] of tagIndex.entries()) {
|
|
43
|
+
const matched = entryTags.some((subject) => tags.some((pattern) => invalidationTagMatches(subject, pattern)));
|
|
44
|
+
if (!matched)
|
|
45
|
+
continue;
|
|
46
|
+
const stored = await storage.get(storageKey(cacheKey));
|
|
47
|
+
if (stored === undefined)
|
|
48
|
+
continue;
|
|
49
|
+
await storage.set(storageKey(cacheKey), {
|
|
50
|
+
...stored,
|
|
51
|
+
status: "stale",
|
|
52
|
+
staleAt: now
|
|
53
|
+
});
|
|
54
|
+
affected.push(cacheKey);
|
|
55
|
+
}
|
|
56
|
+
return affected;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// src/engine/conflict-registry.ts
|
|
61
|
+
function fieldMerge(ctx) {
|
|
62
|
+
const server = ctx.serverData;
|
|
63
|
+
const local = ctx.localData;
|
|
64
|
+
const fields = ctx.mergeFields ?? [];
|
|
65
|
+
if (typeof server !== "object" || server === null || typeof local !== "object" || local === null) {
|
|
66
|
+
return { outcome: "surface" };
|
|
67
|
+
}
|
|
68
|
+
const merged = {
|
|
69
|
+
...server
|
|
70
|
+
};
|
|
71
|
+
const localRecord = local;
|
|
72
|
+
for (const field of fields) {
|
|
73
|
+
if (field in localRecord)
|
|
74
|
+
merged[field] = localRecord[field];
|
|
75
|
+
}
|
|
76
|
+
return { outcome: "merged", data: merged };
|
|
77
|
+
}
|
|
78
|
+
function createConflictResolverRegistry() {
|
|
79
|
+
const resolvers = new Map;
|
|
80
|
+
return {
|
|
81
|
+
register(key, resolver) {
|
|
82
|
+
resolvers.set(key, resolver);
|
|
83
|
+
},
|
|
84
|
+
has(key) {
|
|
85
|
+
return resolvers.has(key);
|
|
86
|
+
},
|
|
87
|
+
async resolve(ctx) {
|
|
88
|
+
switch (ctx.strategy) {
|
|
89
|
+
case "server-wins":
|
|
90
|
+
return { outcome: "use-server", data: ctx.serverData };
|
|
91
|
+
case "client-wins":
|
|
92
|
+
return { outcome: "use-client", data: ctx.localData };
|
|
93
|
+
case "field-merge":
|
|
94
|
+
return fieldMerge(ctx);
|
|
95
|
+
case "surface":
|
|
96
|
+
return { outcome: "surface" };
|
|
97
|
+
default: {
|
|
98
|
+
const key = ctx.policy.resolver;
|
|
99
|
+
const resolver = key === undefined ? undefined : resolvers.get(key);
|
|
100
|
+
if (resolver === undefined)
|
|
101
|
+
return { outcome: "surface" };
|
|
102
|
+
return resolver(ctx);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// src/engine/conflict-resolution.ts
|
|
109
|
+
import { defaultConflictPolicy } from "@lssm/lib.contracts-spec/query/conflict-policy";
|
|
110
|
+
async function restoreSnapshots(snapshots, cache) {
|
|
111
|
+
for (const snapshot of snapshots) {
|
|
112
|
+
if (snapshot.previous === undefined)
|
|
113
|
+
await cache.remove(snapshot.key);
|
|
114
|
+
else
|
|
115
|
+
await cache.write(snapshot.previous);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function resolveQueuedConflict(entry, problem, deps) {
|
|
119
|
+
const policy = entry.conflictPolicy ?? defaultConflictPolicy();
|
|
120
|
+
const primaryKey = entry.rollback[0]?.key;
|
|
121
|
+
const localEntry = primaryKey === undefined ? undefined : await deps.cache.read(primaryKey);
|
|
122
|
+
const serverData = problem.metadata?.serverData;
|
|
123
|
+
const resolution = await deps.resolvers.resolve({
|
|
124
|
+
strategy: policy.strategy,
|
|
125
|
+
policy,
|
|
126
|
+
localData: localEntry?.data,
|
|
127
|
+
serverData,
|
|
128
|
+
localVersion: entry.versionToken,
|
|
129
|
+
serverVersion: problem.versionToken,
|
|
130
|
+
mergeFields: policy.mergeFields
|
|
131
|
+
});
|
|
132
|
+
deps.events.emit({
|
|
133
|
+
type: "mutation-conflict",
|
|
134
|
+
id: entry.id,
|
|
135
|
+
strategy: policy.strategy,
|
|
136
|
+
outcome: resolution.outcome
|
|
137
|
+
});
|
|
138
|
+
switch (resolution.outcome) {
|
|
139
|
+
case "use-server": {
|
|
140
|
+
await restoreSnapshots(entry.rollback, deps.cache);
|
|
141
|
+
await deps.cache.invalidate(entry.tags, deps.now());
|
|
142
|
+
for (const snapshot of entry.rollback) {
|
|
143
|
+
await deps.queryEngine?.refresh(snapshot.key);
|
|
144
|
+
}
|
|
145
|
+
return "resolved";
|
|
146
|
+
}
|
|
147
|
+
case "use-client":
|
|
148
|
+
await deps.queue.update({
|
|
149
|
+
...entry,
|
|
150
|
+
versionToken: problem.versionToken,
|
|
151
|
+
status: "pending"
|
|
152
|
+
});
|
|
153
|
+
return "re-enqueued";
|
|
154
|
+
case "merged": {
|
|
155
|
+
if (primaryKey !== undefined && localEntry !== undefined) {
|
|
156
|
+
await deps.cache.write({
|
|
157
|
+
...localEntry,
|
|
158
|
+
data: resolution.data,
|
|
159
|
+
versionToken: problem.versionToken,
|
|
160
|
+
status: "warm",
|
|
161
|
+
storedAt: deps.now()
|
|
162
|
+
});
|
|
163
|
+
await deps.queryEngine?.refresh(primaryKey);
|
|
164
|
+
}
|
|
165
|
+
await deps.queue.update({
|
|
166
|
+
...entry,
|
|
167
|
+
versionToken: problem.versionToken,
|
|
168
|
+
status: "pending"
|
|
169
|
+
});
|
|
170
|
+
return "re-enqueued";
|
|
171
|
+
}
|
|
172
|
+
default: {
|
|
173
|
+
if (primaryKey !== undefined) {
|
|
174
|
+
await deps.queryEngine?.emitConflict(primaryKey, {
|
|
175
|
+
versionToken: problem.versionToken,
|
|
176
|
+
serverData,
|
|
177
|
+
clientData: localEntry?.data,
|
|
178
|
+
strategy: policy.strategy
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return "resolved";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// src/local-offline.ts
|
|
186
|
+
var MODES = new Set([
|
|
187
|
+
"online-only",
|
|
188
|
+
"memory-cache",
|
|
189
|
+
"offline-capable"
|
|
190
|
+
]);
|
|
191
|
+
var MODE_ALIASES = {
|
|
192
|
+
"online-only": "online-only",
|
|
193
|
+
online: "online-only",
|
|
194
|
+
disabled: "online-only",
|
|
195
|
+
false: "online-only",
|
|
196
|
+
off: "online-only",
|
|
197
|
+
"memory-cache": "memory-cache",
|
|
198
|
+
memory: "memory-cache",
|
|
199
|
+
cache: "memory-cache",
|
|
200
|
+
"offline-capable": "offline-capable",
|
|
201
|
+
offline: "offline-capable",
|
|
202
|
+
enabled: "offline-capable",
|
|
203
|
+
true: "offline-capable",
|
|
204
|
+
on: "offline-capable"
|
|
205
|
+
};
|
|
206
|
+
function isLocalOfflineMode(value) {
|
|
207
|
+
return typeof value === "string" && MODES.has(value);
|
|
208
|
+
}
|
|
209
|
+
function resolveLocalOfflinePolicy(options = {}) {
|
|
210
|
+
const fallback = options.defaultMode === undefined ? {
|
|
211
|
+
mode: "online-only",
|
|
212
|
+
source: "default",
|
|
213
|
+
reason: "No local/offline policy input was supplied."
|
|
214
|
+
} : normalizePolicy(options.defaultMode, "explicit") ?? {
|
|
215
|
+
mode: "online-only",
|
|
216
|
+
source: "explicit"
|
|
217
|
+
};
|
|
218
|
+
const env = normalizePolicy(readEnvValue(options.env), "env", {
|
|
219
|
+
allowUnknown: true
|
|
220
|
+
});
|
|
221
|
+
const flag = normalizePolicy(options.flag, "feature-flag");
|
|
222
|
+
return flag ?? env ?? fallback;
|
|
223
|
+
}
|
|
224
|
+
function readEnvValue(input) {
|
|
225
|
+
if (typeof input !== "object" || input === null)
|
|
226
|
+
return input;
|
|
227
|
+
if ("CONTRACTSPEC_LOCAL_OFFLINE" in input) {
|
|
228
|
+
return input.CONTRACTSPEC_LOCAL_OFFLINE;
|
|
229
|
+
}
|
|
230
|
+
if ("mode" in input)
|
|
231
|
+
return input;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
function normalizePolicy(input, source, options = {}) {
|
|
235
|
+
if (input === undefined || input === null || input === "")
|
|
236
|
+
return;
|
|
237
|
+
if (typeof input === "boolean") {
|
|
238
|
+
return { mode: input ? "offline-capable" : "online-only", source };
|
|
239
|
+
}
|
|
240
|
+
if (typeof input === "string") {
|
|
241
|
+
const mode = MODE_ALIASES[input.trim().toLowerCase()];
|
|
242
|
+
if (mode !== undefined)
|
|
243
|
+
return { mode, source };
|
|
244
|
+
if (options.allowUnknown)
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (typeof input === "object" && input !== null && "mode" in input) {
|
|
248
|
+
const policy = input;
|
|
249
|
+
const mode = typeof policy.mode === "string" ? MODE_ALIASES[policy.mode.trim().toLowerCase()] : undefined;
|
|
250
|
+
if (mode !== undefined) {
|
|
251
|
+
return {
|
|
252
|
+
mode,
|
|
253
|
+
source,
|
|
254
|
+
...typeof policy.reason === "string" ? { reason: policy.reason } : {}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (options.allowUnknown)
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
throw new Error(`Unsupported localOffline policy value: ${String(input)}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/reachability.ts
|
|
264
|
+
function isNetworkAllowed(signal) {
|
|
265
|
+
return signal !== "offline";
|
|
266
|
+
}
|
|
267
|
+
function createStaticReachabilitySource(signal = "online") {
|
|
268
|
+
const snapshot = { signal, since: Date.now() };
|
|
269
|
+
return {
|
|
270
|
+
getSnapshot: () => snapshot,
|
|
271
|
+
subscribe: () => () => {}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
var reachabilityPortDoc = {
|
|
275
|
+
id: "docs.contracts-runtime-core.reachability",
|
|
276
|
+
title: "Reachability input port (tri-state)",
|
|
277
|
+
summary: "Engine-owned reachability input port; integrations feed it, the engine never imports them.",
|
|
278
|
+
kind: "reference",
|
|
279
|
+
visibility: "internal",
|
|
280
|
+
route: "/docs/contracts-runtime-core/reachability",
|
|
281
|
+
domain: "contracts-runtime-core",
|
|
282
|
+
owners: ["@contractspec-core"],
|
|
283
|
+
tags: ["contracts-runtime-core", "reachability", "offline", "port"],
|
|
284
|
+
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."
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// src/storage/memory.ts
|
|
288
|
+
function createMemoryStorage() {
|
|
289
|
+
const store = new Map;
|
|
290
|
+
return {
|
|
291
|
+
get: async (key) => {
|
|
292
|
+
const value = store.get(key);
|
|
293
|
+
return value === undefined ? undefined : structuredClone(value);
|
|
294
|
+
},
|
|
295
|
+
set: async (key, value) => {
|
|
296
|
+
store.set(key, structuredClone(value));
|
|
297
|
+
},
|
|
298
|
+
remove: async (key) => {
|
|
299
|
+
store.delete(key);
|
|
300
|
+
},
|
|
301
|
+
keys: async (prefix) => {
|
|
302
|
+
const all = [...store.keys()];
|
|
303
|
+
return prefix === undefined ? all : all.filter((key) => key.startsWith(prefix));
|
|
304
|
+
},
|
|
305
|
+
clear: async () => {
|
|
306
|
+
store.clear();
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/engine/events.ts
|
|
312
|
+
function createEngineEvents() {
|
|
313
|
+
const listeners = new Set;
|
|
314
|
+
return {
|
|
315
|
+
emit(event) {
|
|
316
|
+
for (const listener of listeners)
|
|
317
|
+
listener(event);
|
|
318
|
+
},
|
|
319
|
+
subscribe(listener) {
|
|
320
|
+
listeners.add(listener);
|
|
321
|
+
return () => {
|
|
322
|
+
listeners.delete(listener);
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/engine/maintenance.ts
|
|
329
|
+
function createMaintenance(deps) {
|
|
330
|
+
return {
|
|
331
|
+
async collectGarbage() {
|
|
332
|
+
const ts = deps.now();
|
|
333
|
+
let evicted = 0;
|
|
334
|
+
for (const entry of await deps.cache.entries()) {
|
|
335
|
+
if (entry.expiresAt !== undefined && ts >= entry.expiresAt) {
|
|
336
|
+
await deps.cache.remove(entry.key);
|
|
337
|
+
evicted += 1;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
deps.events.emit({ type: "gc-collected", evicted });
|
|
341
|
+
return evicted;
|
|
342
|
+
},
|
|
343
|
+
async revalidateStale() {
|
|
344
|
+
const revalidate = deps.revalidate;
|
|
345
|
+
if (revalidate === undefined)
|
|
346
|
+
return 0;
|
|
347
|
+
const ts = deps.now();
|
|
348
|
+
let count = 0;
|
|
349
|
+
for (const entry of await deps.cache.entries()) {
|
|
350
|
+
const stale = entry.staleAt !== undefined && ts >= entry.staleAt;
|
|
351
|
+
if (!stale || entry.envelope === undefined)
|
|
352
|
+
continue;
|
|
353
|
+
await revalidate(entry.envelope);
|
|
354
|
+
deps.events.emit({ type: "revalidated", key: entry.key });
|
|
355
|
+
count += 1;
|
|
356
|
+
}
|
|
357
|
+
return count;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/engine/mutation-engine.ts
|
|
363
|
+
import {
|
|
364
|
+
mergeSafeEconomicEvidenceRefIds,
|
|
365
|
+
parseSafeEconomicEvidenceRefId
|
|
366
|
+
} from "@lssm/lib.contracts-spec/operations";
|
|
367
|
+
import { defaultConflictPolicy as defaultConflictPolicy2 } from "@lssm/lib.contracts-spec/query/conflict-policy";
|
|
368
|
+
var DEFAULT_MAX_ATTEMPTS = 5;
|
|
369
|
+
function mergeEconomicEvidenceRefs(...groups) {
|
|
370
|
+
return mergeSafeEconomicEvidenceRefIds(...groups);
|
|
371
|
+
}
|
|
372
|
+
function toRuntimeEconomicEvidenceRef(ref) {
|
|
373
|
+
const safeRef = parseSafeEconomicEvidenceRefId(ref);
|
|
374
|
+
if (!safeRef) {
|
|
375
|
+
throw new Error("EconomicEvidenceInvalidRef: unsafe or invalid economic evidence ref");
|
|
376
|
+
}
|
|
377
|
+
return safeRef;
|
|
378
|
+
}
|
|
379
|
+
function createMutationEngine(options) {
|
|
380
|
+
const { transport, cache, queue, reachability, resolvers, events } = options;
|
|
381
|
+
const now = options.now ?? (() => Date.now());
|
|
382
|
+
const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
383
|
+
const deps = {
|
|
384
|
+
cache,
|
|
385
|
+
queue,
|
|
386
|
+
resolvers,
|
|
387
|
+
events,
|
|
388
|
+
now,
|
|
389
|
+
queryEngine: options.queryEngine
|
|
390
|
+
};
|
|
391
|
+
let paused = false;
|
|
392
|
+
let seq = 0;
|
|
393
|
+
let draining = null;
|
|
394
|
+
const outcomes = new Map;
|
|
395
|
+
const toEnvelope = (entry, economicEvidenceRefs = entry.economicEvidenceRefs) => ({
|
|
396
|
+
operation: entry.operation,
|
|
397
|
+
input: entry.input,
|
|
398
|
+
tags: entry.tags,
|
|
399
|
+
economicEvidenceRefs
|
|
400
|
+
});
|
|
401
|
+
async function applyOptimistic(writes) {
|
|
402
|
+
const snapshots = [];
|
|
403
|
+
for (const write of writes) {
|
|
404
|
+
const previous = await cache.read(write.key);
|
|
405
|
+
snapshots.push({ key: write.key, previous });
|
|
406
|
+
await cache.write({
|
|
407
|
+
key: write.key,
|
|
408
|
+
data: write.data,
|
|
409
|
+
versionToken: write.versionToken ?? previous?.versionToken,
|
|
410
|
+
status: "warm",
|
|
411
|
+
tags: previous?.tags ?? [],
|
|
412
|
+
storedAt: now(),
|
|
413
|
+
staleAt: previous?.staleAt,
|
|
414
|
+
expiresAt: previous?.expiresAt
|
|
415
|
+
});
|
|
416
|
+
await options.queryEngine?.refresh(write.key);
|
|
417
|
+
}
|
|
418
|
+
return snapshots;
|
|
419
|
+
}
|
|
420
|
+
async function commitSuccess(entry, result) {
|
|
421
|
+
for (const snapshot of entry.rollback) {
|
|
422
|
+
const current = await cache.read(snapshot.key);
|
|
423
|
+
if (current !== undefined) {
|
|
424
|
+
await cache.write({
|
|
425
|
+
...current,
|
|
426
|
+
versionToken: result.versionToken ?? current.versionToken,
|
|
427
|
+
status: "stale",
|
|
428
|
+
staleAt: now()
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
await options.queryEngine?.refresh(snapshot.key);
|
|
432
|
+
}
|
|
433
|
+
await cache.invalidate(entry.tags, now());
|
|
434
|
+
}
|
|
435
|
+
async function replayEntry(entry) {
|
|
436
|
+
let safeEntryEconomicEvidenceRefs;
|
|
437
|
+
try {
|
|
438
|
+
safeEntryEconomicEvidenceRefs = mergeEconomicEvidenceRefs(entry.economicEvidenceRefs);
|
|
439
|
+
} catch {
|
|
440
|
+
await restoreSnapshots(entry.rollback, cache);
|
|
441
|
+
for (const snapshot of entry.rollback) {
|
|
442
|
+
await options.queryEngine?.refresh(snapshot.key);
|
|
443
|
+
}
|
|
444
|
+
await queue.remove(entry.id);
|
|
445
|
+
events.emit({
|
|
446
|
+
type: "mutation-failed",
|
|
447
|
+
id: entry.id,
|
|
448
|
+
code: "ECONOMIC_EVIDENCE_INVALID_REF"
|
|
449
|
+
});
|
|
450
|
+
outcomes.set(entry.id, {
|
|
451
|
+
id: entry.id,
|
|
452
|
+
status: "failed",
|
|
453
|
+
problem: {
|
|
454
|
+
type: "about:blank",
|
|
455
|
+
title: "Economic evidence ref rejected",
|
|
456
|
+
status: 422,
|
|
457
|
+
code: "ECONOMIC_EVIDENCE_INVALID_REF",
|
|
458
|
+
category: "validation",
|
|
459
|
+
severity: "error",
|
|
460
|
+
retryable: false
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
const attempts = entry.attempts + 1;
|
|
466
|
+
await queue.update({ ...entry, status: "in-flight", attempts });
|
|
467
|
+
const result = await transport.execute(toEnvelope(entry, safeEntryEconomicEvidenceRefs), options.contextProvider?.());
|
|
468
|
+
if (result.ok) {
|
|
469
|
+
const economicEvidenceRefs = mergeEconomicEvidenceRefs(safeEntryEconomicEvidenceRefs, result.data.economicEvidenceRefs);
|
|
470
|
+
await commitSuccess(entry, result.data);
|
|
471
|
+
await queue.remove(entry.id);
|
|
472
|
+
events.emit({
|
|
473
|
+
type: "mutation-replayed",
|
|
474
|
+
id: entry.id,
|
|
475
|
+
queueDepth: await queue.depth()
|
|
476
|
+
});
|
|
477
|
+
if (economicEvidenceRefs.length > 0) {
|
|
478
|
+
events.emit({
|
|
479
|
+
type: "economic-evidence-attached",
|
|
480
|
+
operationKey: entry.operation.key,
|
|
481
|
+
evidenceRefs: economicEvidenceRefs.map(toRuntimeEconomicEvidenceRef)
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
outcomes.set(entry.id, {
|
|
485
|
+
id: entry.id,
|
|
486
|
+
status: "done",
|
|
487
|
+
data: result.data.data,
|
|
488
|
+
economicEvidenceRefs
|
|
489
|
+
});
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
const { problem } = result;
|
|
493
|
+
if (problem.category === "auth") {
|
|
494
|
+
paused = true;
|
|
495
|
+
await queue.update({ ...entry, status: "paused-auth", attempts });
|
|
496
|
+
events.emit({ type: "auth-expired", id: entry.id });
|
|
497
|
+
outcomes.set(entry.id, { id: entry.id, status: "paused-auth", problem });
|
|
498
|
+
if (options.refreshAuth !== undefined && await options.refreshAuth()) {
|
|
499
|
+
paused = false;
|
|
500
|
+
await queue.update({ ...entry, status: "pending", attempts });
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
if (problem.category === "conflict") {
|
|
506
|
+
const disposition = await resolveQueuedConflict(entry, problem, deps);
|
|
507
|
+
if (disposition === "resolved") {
|
|
508
|
+
await queue.remove(entry.id);
|
|
509
|
+
outcomes.set(entry.id, { id: entry.id, status: "done", problem });
|
|
510
|
+
}
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
if (problem.retryable && attempts < maxAttempts) {
|
|
514
|
+
const delayMs = backoffDelayMs(attempts, options.backoff);
|
|
515
|
+
await queue.update({ ...entry, status: "failed", attempts });
|
|
516
|
+
events.emit({
|
|
517
|
+
type: "mutation-retry",
|
|
518
|
+
id: entry.id,
|
|
519
|
+
attempt: attempts,
|
|
520
|
+
delayMs
|
|
521
|
+
});
|
|
522
|
+
outcomes.set(entry.id, { id: entry.id, status: "failed", problem });
|
|
523
|
+
options.schedule?.(() => {
|
|
524
|
+
drain();
|
|
525
|
+
}, delayMs);
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
await restoreSnapshots(entry.rollback, cache);
|
|
529
|
+
for (const snapshot of entry.rollback) {
|
|
530
|
+
await options.queryEngine?.refresh(snapshot.key);
|
|
531
|
+
}
|
|
532
|
+
await queue.remove(entry.id);
|
|
533
|
+
events.emit({ type: "mutation-failed", id: entry.id, code: result.code });
|
|
534
|
+
outcomes.set(entry.id, { id: entry.id, status: "failed", problem });
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
async function runDrain() {
|
|
538
|
+
if (paused)
|
|
539
|
+
return;
|
|
540
|
+
if (!isNetworkAllowed(reachability.getSnapshot().signal))
|
|
541
|
+
return;
|
|
542
|
+
for (const entry of await queue.list()) {
|
|
543
|
+
if (entry.status === "paused-auth")
|
|
544
|
+
continue;
|
|
545
|
+
if (!await replayEntry(entry))
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
events.emit({ type: "queue-drained", remaining: await queue.depth() });
|
|
549
|
+
}
|
|
550
|
+
function drain() {
|
|
551
|
+
if (draining !== null)
|
|
552
|
+
return draining;
|
|
553
|
+
draining = runDrain().finally(() => {
|
|
554
|
+
draining = null;
|
|
555
|
+
});
|
|
556
|
+
return draining;
|
|
557
|
+
}
|
|
558
|
+
async function mutate(request, mutateOptions = {}) {
|
|
559
|
+
mergeEconomicEvidenceRefs(request.economicEvidenceRefs);
|
|
560
|
+
const enqueuedAt = now();
|
|
561
|
+
seq += 1;
|
|
562
|
+
const id = `${enqueuedAt}-${seq}`;
|
|
563
|
+
const rollback = mutateOptions.optimistic ? await applyOptimistic(mutateOptions.optimistic) : [];
|
|
564
|
+
await queue.enqueue({
|
|
565
|
+
...request,
|
|
566
|
+
conflictPolicy: request.conflictPolicy ?? defaultConflictPolicy2(),
|
|
567
|
+
id,
|
|
568
|
+
enqueuedAt,
|
|
569
|
+
attempts: 0,
|
|
570
|
+
status: "pending",
|
|
571
|
+
rollback
|
|
572
|
+
});
|
|
573
|
+
events.emit({
|
|
574
|
+
type: "mutation-enqueued",
|
|
575
|
+
id,
|
|
576
|
+
queueDepth: await queue.depth()
|
|
577
|
+
});
|
|
578
|
+
if (!paused && isNetworkAllowed(reachability.getSnapshot().signal)) {
|
|
579
|
+
await drain();
|
|
580
|
+
}
|
|
581
|
+
return outcomes.get(id) ?? {
|
|
582
|
+
id,
|
|
583
|
+
status: "pending"
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
async function resume() {
|
|
587
|
+
paused = false;
|
|
588
|
+
for (const entry of await queue.list()) {
|
|
589
|
+
if (entry.status === "paused-auth") {
|
|
590
|
+
await queue.update({ ...entry, status: "pending" });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
await drain();
|
|
594
|
+
}
|
|
595
|
+
const unsubscribe = reachability.subscribe((snapshot) => {
|
|
596
|
+
if (!paused && isNetworkAllowed(snapshot.signal))
|
|
597
|
+
drain();
|
|
598
|
+
});
|
|
599
|
+
return { mutate, drain, resume, dispose: unsubscribe };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/engine/mutation-queue.ts
|
|
603
|
+
var QUEUE_PREFIX = "queue:";
|
|
604
|
+
function createMutationQueue(storage) {
|
|
605
|
+
const storageKey = (id) => `${QUEUE_PREFIX}${id}`;
|
|
606
|
+
async function readAll() {
|
|
607
|
+
const keys = await storage.keys(QUEUE_PREFIX);
|
|
608
|
+
const entries = [];
|
|
609
|
+
for (const key of keys) {
|
|
610
|
+
const entry = await storage.get(key);
|
|
611
|
+
if (entry !== undefined)
|
|
612
|
+
entries.push(entry);
|
|
613
|
+
}
|
|
614
|
+
entries.sort((a, b) => a.enqueuedAt === b.enqueuedAt ? a.id.localeCompare(b.id) : a.enqueuedAt - b.enqueuedAt);
|
|
615
|
+
return entries;
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
enqueue: (entry) => storage.set(storageKey(entry.id), entry),
|
|
619
|
+
update: (entry) => storage.set(storageKey(entry.id), entry),
|
|
620
|
+
remove: (id) => storage.remove(storageKey(id)),
|
|
621
|
+
list: () => readAll(),
|
|
622
|
+
depth: async () => (await storage.keys(QUEUE_PREFIX)).length
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/engine/online-only-data-engine.ts
|
|
627
|
+
import { createQueryKey } from "@lssm/lib.contracts-spec/query/key";
|
|
628
|
+
|
|
629
|
+
// src/engine/state.ts
|
|
630
|
+
function computeIsStale(entry, now) {
|
|
631
|
+
if (entry?.staleAt === undefined)
|
|
632
|
+
return false;
|
|
633
|
+
return now >= entry.staleAt;
|
|
634
|
+
}
|
|
635
|
+
function idleState() {
|
|
636
|
+
return {
|
|
637
|
+
status: "idle",
|
|
638
|
+
fetchStatus: "idle",
|
|
639
|
+
cacheStatus: "empty",
|
|
640
|
+
isStale: false,
|
|
641
|
+
isOffline: false
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function loadingState(entry, now) {
|
|
645
|
+
if (entry === undefined) {
|
|
646
|
+
return {
|
|
647
|
+
status: "loading",
|
|
648
|
+
fetchStatus: "fetching",
|
|
649
|
+
cacheStatus: "empty",
|
|
650
|
+
isStale: false,
|
|
651
|
+
isOffline: false
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
const isStale = computeIsStale(entry, now);
|
|
655
|
+
return {
|
|
656
|
+
status: "success",
|
|
657
|
+
fetchStatus: "fetching",
|
|
658
|
+
data: entry.data,
|
|
659
|
+
cacheStatus: isStale ? "stale" : entry.status,
|
|
660
|
+
isStale,
|
|
661
|
+
isOffline: false,
|
|
662
|
+
versionToken: entry.versionToken,
|
|
663
|
+
dataUpdatedAt: entry.storedAt,
|
|
664
|
+
...entry.pageInfo === undefined ? {} : { pageInfo: entry.pageInfo }
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function successState(entry, now) {
|
|
668
|
+
const isStale = computeIsStale(entry, now);
|
|
669
|
+
return {
|
|
670
|
+
status: "success",
|
|
671
|
+
fetchStatus: "idle",
|
|
672
|
+
data: entry.data,
|
|
673
|
+
cacheStatus: isStale ? "stale" : entry.status,
|
|
674
|
+
isStale,
|
|
675
|
+
isOffline: false,
|
|
676
|
+
versionToken: entry.versionToken,
|
|
677
|
+
dataUpdatedAt: entry.storedAt,
|
|
678
|
+
...entry.pageInfo === undefined ? {} : { pageInfo: entry.pageInfo }
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function errorState(problem, entry, now) {
|
|
682
|
+
return {
|
|
683
|
+
status: "error",
|
|
684
|
+
fetchStatus: "idle",
|
|
685
|
+
data: entry?.data,
|
|
686
|
+
error: problem,
|
|
687
|
+
cacheStatus: entry === undefined ? "empty" : entry.status,
|
|
688
|
+
isStale: computeIsStale(entry, now),
|
|
689
|
+
isOffline: false,
|
|
690
|
+
versionToken: entry?.versionToken,
|
|
691
|
+
dataUpdatedAt: entry?.storedAt,
|
|
692
|
+
errorUpdatedAt: now
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function authExpiredState(problem, entry, now) {
|
|
696
|
+
return {
|
|
697
|
+
status: "auth-expired",
|
|
698
|
+
fetchStatus: "idle",
|
|
699
|
+
data: entry?.data,
|
|
700
|
+
error: problem,
|
|
701
|
+
cacheStatus: entry === undefined ? "empty" : entry.status,
|
|
702
|
+
isStale: computeIsStale(entry, now),
|
|
703
|
+
isOffline: false,
|
|
704
|
+
versionToken: entry?.versionToken,
|
|
705
|
+
dataUpdatedAt: entry?.storedAt,
|
|
706
|
+
errorUpdatedAt: now
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function conflictState(entry, conflict, now) {
|
|
710
|
+
return {
|
|
711
|
+
status: entry === undefined ? "idle" : "success",
|
|
712
|
+
fetchStatus: "idle",
|
|
713
|
+
data: entry?.data,
|
|
714
|
+
cacheStatus: "conflict",
|
|
715
|
+
isStale: computeIsStale(entry, now),
|
|
716
|
+
isOffline: false,
|
|
717
|
+
conflict,
|
|
718
|
+
versionToken: conflict.versionToken ?? entry?.versionToken,
|
|
719
|
+
dataUpdatedAt: entry?.storedAt
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function offlineState(entry, now) {
|
|
723
|
+
return {
|
|
724
|
+
status: entry === undefined ? "idle" : "success",
|
|
725
|
+
fetchStatus: "idle",
|
|
726
|
+
data: entry?.data,
|
|
727
|
+
cacheStatus: "offline",
|
|
728
|
+
isStale: computeIsStale(entry, now),
|
|
729
|
+
isOffline: true,
|
|
730
|
+
versionToken: entry?.versionToken,
|
|
731
|
+
dataUpdatedAt: entry?.storedAt,
|
|
732
|
+
...entry?.pageInfo === undefined ? {} : { pageInfo: entry.pageInfo }
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/engine/online-only-data-engine.ts
|
|
737
|
+
function createOnlineOnlyDataEngine(options, resolvedPolicy) {
|
|
738
|
+
const { transport } = options;
|
|
739
|
+
const advanced = options.advanced ?? {};
|
|
740
|
+
const now = advanced.now ?? (() => Date.now());
|
|
741
|
+
const reachability = advanced.reachability ?? createStaticReachabilitySource("online");
|
|
742
|
+
const events = advanced.events ?? createEngineEvents();
|
|
743
|
+
const resolvers = advanced.resolvers ?? createConflictResolverRegistry();
|
|
744
|
+
const localOffline = resolvedPolicy ?? resolveLocalOfflinePolicy({
|
|
745
|
+
defaultMode: options.localOffline,
|
|
746
|
+
env: options.env,
|
|
747
|
+
flag: options.flag
|
|
748
|
+
});
|
|
749
|
+
const states = new Map;
|
|
750
|
+
const listeners = new Map;
|
|
751
|
+
let seq = 0;
|
|
752
|
+
function emit(key, state) {
|
|
753
|
+
states.set(key, state);
|
|
754
|
+
for (const listener of listeners.get(key) ?? [])
|
|
755
|
+
listener(state);
|
|
756
|
+
}
|
|
757
|
+
const queryEngine = {
|
|
758
|
+
async query(envelope, ctx) {
|
|
759
|
+
const key = createQueryKey(envelope);
|
|
760
|
+
const signal = reachability.getSnapshot().signal;
|
|
761
|
+
if (!isNetworkAllowed(signal)) {
|
|
762
|
+
const state = offlineState(undefined, now());
|
|
763
|
+
emit(key, state);
|
|
764
|
+
return state;
|
|
765
|
+
}
|
|
766
|
+
emit(key, loadingState(undefined, now()));
|
|
767
|
+
const startedAt = now();
|
|
768
|
+
const result = await transport.execute(envelope, ctx);
|
|
769
|
+
const settledAt = now();
|
|
770
|
+
if (!result.ok) {
|
|
771
|
+
events.emit({
|
|
772
|
+
type: "fetch-error",
|
|
773
|
+
key,
|
|
774
|
+
code: result.code,
|
|
775
|
+
latencyMs: settledAt - startedAt
|
|
776
|
+
});
|
|
777
|
+
const state = result.problem.category === "auth" ? authExpiredState(result.problem, undefined, settledAt) : errorState(result.problem, undefined, settledAt);
|
|
778
|
+
emit(key, state);
|
|
779
|
+
return state;
|
|
780
|
+
}
|
|
781
|
+
events.emit({
|
|
782
|
+
type: "fetch-success",
|
|
783
|
+
key,
|
|
784
|
+
latencyMs: settledAt - startedAt
|
|
785
|
+
});
|
|
786
|
+
const entry = {
|
|
787
|
+
key,
|
|
788
|
+
data: result.data.data,
|
|
789
|
+
versionToken: result.data.versionToken ?? result.versionToken,
|
|
790
|
+
status: signal === "restricted" ? "warm" : "fresh",
|
|
791
|
+
tags: [],
|
|
792
|
+
storedAt: settledAt,
|
|
793
|
+
...result.data.pageInfo === undefined ? {} : { pageInfo: result.data.pageInfo }
|
|
794
|
+
};
|
|
795
|
+
const state = successState(entry, settledAt);
|
|
796
|
+
emit(key, state);
|
|
797
|
+
return state;
|
|
798
|
+
},
|
|
799
|
+
getState(envelope) {
|
|
800
|
+
return states.get(createQueryKey(envelope)) ?? idleState();
|
|
801
|
+
},
|
|
802
|
+
subscribe(envelope, listener) {
|
|
803
|
+
const key = createQueryKey(envelope);
|
|
804
|
+
let set = listeners.get(key);
|
|
805
|
+
if (set === undefined) {
|
|
806
|
+
set = new Set;
|
|
807
|
+
listeners.set(key, set);
|
|
808
|
+
}
|
|
809
|
+
set.add(listener);
|
|
810
|
+
return () => set?.delete(listener);
|
|
811
|
+
},
|
|
812
|
+
async invalidateTags() {
|
|
813
|
+
return [];
|
|
814
|
+
},
|
|
815
|
+
async refresh(key) {
|
|
816
|
+
const state = states.get(key) ?? idleState();
|
|
817
|
+
emit(key, state);
|
|
818
|
+
return state;
|
|
819
|
+
},
|
|
820
|
+
async emitConflict(key, conflict) {
|
|
821
|
+
const state = conflictState(undefined, conflict, now());
|
|
822
|
+
emit(key, state);
|
|
823
|
+
return state;
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
const mutationEngine = {
|
|
827
|
+
async mutate(request, mutateOptions = {}) {
|
|
828
|
+
seq += 1;
|
|
829
|
+
const id = `${now()}-${seq}`;
|
|
830
|
+
if (!isNetworkAllowed(reachability.getSnapshot().signal)) {
|
|
831
|
+
return {
|
|
832
|
+
id,
|
|
833
|
+
status: "disabled-offline",
|
|
834
|
+
problem: offlineQueueDisabledProblem()
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
const result = await transport.execute({
|
|
838
|
+
operation: request.operation,
|
|
839
|
+
input: request.input,
|
|
840
|
+
tags: request.tags
|
|
841
|
+
}, mutateOptions.ctx ?? advanced.contextProvider?.());
|
|
842
|
+
if (result.ok)
|
|
843
|
+
return { id, status: "done", data: result.data.data };
|
|
844
|
+
if (result.problem.category === "auth") {
|
|
845
|
+
return { id, status: "paused-auth", problem: result.problem };
|
|
846
|
+
}
|
|
847
|
+
return { id, status: "failed", problem: result.problem };
|
|
848
|
+
},
|
|
849
|
+
async drain() {},
|
|
850
|
+
async resume() {},
|
|
851
|
+
dispose() {}
|
|
852
|
+
};
|
|
853
|
+
return {
|
|
854
|
+
...queryEngine,
|
|
855
|
+
...mutationEngine,
|
|
856
|
+
maintenance: {
|
|
857
|
+
collectGarbage: async () => 0,
|
|
858
|
+
revalidateStale: async () => 0
|
|
859
|
+
},
|
|
860
|
+
events,
|
|
861
|
+
resolvers,
|
|
862
|
+
localOffline
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
function offlineQueueDisabledProblem() {
|
|
866
|
+
return {
|
|
867
|
+
type: "about:blank",
|
|
868
|
+
title: "Offline mutation queue disabled",
|
|
869
|
+
status: 409,
|
|
870
|
+
code: "OFFLINE_QUEUE_DISABLED",
|
|
871
|
+
category: "conflict",
|
|
872
|
+
severity: "warning",
|
|
873
|
+
retryable: false,
|
|
874
|
+
detail: "localOffline=online-only does not cache local data or queue offline mutations."
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/engine/query-engine.ts
|
|
879
|
+
import { createQueryKey as createQueryKey2 } from "@lssm/lib.contracts-spec/query/key";
|
|
880
|
+
|
|
881
|
+
// src/engine/dedup.ts
|
|
882
|
+
function createInflightRegistry() {
|
|
883
|
+
const inflight = new Map;
|
|
884
|
+
return {
|
|
885
|
+
run(key, factory) {
|
|
886
|
+
const existing = inflight.get(key);
|
|
887
|
+
if (existing !== undefined)
|
|
888
|
+
return existing;
|
|
889
|
+
const promise = factory().finally(() => {
|
|
890
|
+
inflight.delete(key);
|
|
891
|
+
});
|
|
892
|
+
inflight.set(key, promise);
|
|
893
|
+
return promise;
|
|
894
|
+
},
|
|
895
|
+
size() {
|
|
896
|
+
return inflight.size;
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/engine/query-engine.ts
|
|
902
|
+
var DEFAULT_STALE_TIME_MS = 0;
|
|
903
|
+
function createQueryEngine(options) {
|
|
904
|
+
const { transport } = options;
|
|
905
|
+
const storage = options.storage ?? createMemoryStorage();
|
|
906
|
+
const reachability = options.reachability ?? createStaticReachabilitySource("online");
|
|
907
|
+
const now = options.now ?? (() => Date.now());
|
|
908
|
+
const defaultStaleTimeMs = options.defaultStaleTimeMs ?? DEFAULT_STALE_TIME_MS;
|
|
909
|
+
const cache = options.cacheStore ?? createCacheStore(storage);
|
|
910
|
+
const events = options.events ?? createEngineEvents();
|
|
911
|
+
const inflight = createInflightRegistry();
|
|
912
|
+
const states = new Map;
|
|
913
|
+
const listeners = new Map;
|
|
914
|
+
function emit(key, state) {
|
|
915
|
+
states.set(key, state);
|
|
916
|
+
const set = listeners.get(key);
|
|
917
|
+
if (set === undefined)
|
|
918
|
+
return;
|
|
919
|
+
for (const listener of set)
|
|
920
|
+
listener(state);
|
|
921
|
+
}
|
|
922
|
+
async function query(envelope, ctx) {
|
|
923
|
+
const key = createQueryKey2(envelope);
|
|
924
|
+
const signal = reachability.getSnapshot().signal;
|
|
925
|
+
const entry = await cache.read(key);
|
|
926
|
+
if (!isNetworkAllowed(signal)) {
|
|
927
|
+
const state = offlineState(entry, now());
|
|
928
|
+
emit(key, state);
|
|
929
|
+
return state;
|
|
930
|
+
}
|
|
931
|
+
if (entry !== undefined && !computeIsStale(entry, now())) {
|
|
932
|
+
events.emit({ type: "cache-hit", key });
|
|
933
|
+
const state = successState(entry, now());
|
|
934
|
+
emit(key, state);
|
|
935
|
+
return state;
|
|
936
|
+
}
|
|
937
|
+
events.emit({ type: "cache-miss", key });
|
|
938
|
+
emit(key, loadingState(entry, now()));
|
|
939
|
+
const startedAt = now();
|
|
940
|
+
const result = await inflight.run(key, () => transport.execute(envelope, ctx));
|
|
941
|
+
const settledAt = now();
|
|
942
|
+
if (!result.ok) {
|
|
943
|
+
events.emit({
|
|
944
|
+
type: "fetch-error",
|
|
945
|
+
key,
|
|
946
|
+
code: result.code,
|
|
947
|
+
latencyMs: settledAt - startedAt
|
|
948
|
+
});
|
|
949
|
+
const state = result.problem.category === "auth" ? authExpiredState(result.problem, entry, settledAt) : errorState(result.problem, entry, settledAt);
|
|
950
|
+
emit(key, state);
|
|
951
|
+
return state;
|
|
952
|
+
}
|
|
953
|
+
events.emit({
|
|
954
|
+
type: "fetch-success",
|
|
955
|
+
key,
|
|
956
|
+
latencyMs: settledAt - startedAt
|
|
957
|
+
});
|
|
958
|
+
const staleTimeMs = envelope.cache?.staleTimeMs ?? defaultStaleTimeMs;
|
|
959
|
+
const cacheTimeMs = envelope.cache?.cacheTimeMs;
|
|
960
|
+
const written = {
|
|
961
|
+
key,
|
|
962
|
+
data: result.data.data,
|
|
963
|
+
versionToken: result.data.versionToken ?? result.versionToken,
|
|
964
|
+
status: signal === "restricted" ? "warm" : "fresh",
|
|
965
|
+
tags: envelope.tags ?? [],
|
|
966
|
+
storedAt: settledAt,
|
|
967
|
+
staleAt: settledAt + staleTimeMs,
|
|
968
|
+
envelope,
|
|
969
|
+
...cacheTimeMs === undefined ? {} : { expiresAt: settledAt + cacheTimeMs },
|
|
970
|
+
...result.data.pageInfo === undefined ? {} : { pageInfo: result.data.pageInfo }
|
|
971
|
+
};
|
|
972
|
+
await cache.write(written);
|
|
973
|
+
const state = successState(written, settledAt);
|
|
974
|
+
emit(key, state);
|
|
975
|
+
return state;
|
|
976
|
+
}
|
|
977
|
+
async function refresh(key) {
|
|
978
|
+
const entry = await cache.read(key);
|
|
979
|
+
const state = entry === undefined ? idleState() : successState(entry, now());
|
|
980
|
+
emit(key, state);
|
|
981
|
+
return state;
|
|
982
|
+
}
|
|
983
|
+
async function emitConflict(key, conflict) {
|
|
984
|
+
const entry = await cache.read(key);
|
|
985
|
+
const state = conflictState(entry, conflict, now());
|
|
986
|
+
emit(key, state);
|
|
987
|
+
return state;
|
|
988
|
+
}
|
|
989
|
+
function getState(envelope) {
|
|
990
|
+
const existing = states.get(createQueryKey2(envelope));
|
|
991
|
+
return existing ?? idleState();
|
|
992
|
+
}
|
|
993
|
+
function subscribe(envelope, listener) {
|
|
994
|
+
const key = createQueryKey2(envelope);
|
|
995
|
+
let set = listeners.get(key);
|
|
996
|
+
if (set === undefined) {
|
|
997
|
+
set = new Set;
|
|
998
|
+
listeners.set(key, set);
|
|
999
|
+
}
|
|
1000
|
+
set.add(listener);
|
|
1001
|
+
return () => {
|
|
1002
|
+
listeners.get(key)?.delete(listener);
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
async function invalidateTags(tags) {
|
|
1006
|
+
const affected = await cache.invalidate(tags, now());
|
|
1007
|
+
for (const key of affected) {
|
|
1008
|
+
const entry = await cache.read(key);
|
|
1009
|
+
if (entry !== undefined)
|
|
1010
|
+
emit(key, successState(entry, now()));
|
|
1011
|
+
}
|
|
1012
|
+
return affected;
|
|
1013
|
+
}
|
|
1014
|
+
return { query, getState, subscribe, invalidateTags, refresh, emitConflict };
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// src/engine/data-engine.ts
|
|
1018
|
+
function createDataEngine(options) {
|
|
1019
|
+
assertNoLegacyLowLevelOptions(options);
|
|
1020
|
+
const advanced = options.advanced ?? {};
|
|
1021
|
+
const policy = resolveLocalOfflinePolicy({
|
|
1022
|
+
defaultMode: options.localOffline,
|
|
1023
|
+
env: options.env,
|
|
1024
|
+
flag: options.flag
|
|
1025
|
+
});
|
|
1026
|
+
assertModeCompatibleWithAdvanced(policy.mode, advanced);
|
|
1027
|
+
if (policy.mode === "online-only")
|
|
1028
|
+
return createOnlineOnlyDataEngine(options, policy);
|
|
1029
|
+
const now = advanced.now ?? (() => Date.now());
|
|
1030
|
+
const storage = advanced.storage ?? createMemoryStorage();
|
|
1031
|
+
const events = advanced.events ?? createEngineEvents();
|
|
1032
|
+
const cacheStore = advanced.cacheStore ?? createCacheStore(storage);
|
|
1033
|
+
const reachability = advanced.reachability ?? createStaticReachabilitySource("online");
|
|
1034
|
+
const resolvers = advanced.resolvers ?? createConflictResolverRegistry();
|
|
1035
|
+
const queue = createMutationQueue(storage);
|
|
1036
|
+
const queryEngine = createQueryEngine({
|
|
1037
|
+
...advanced,
|
|
1038
|
+
transport: options.transport,
|
|
1039
|
+
now,
|
|
1040
|
+
storage,
|
|
1041
|
+
events,
|
|
1042
|
+
cacheStore,
|
|
1043
|
+
reachability
|
|
1044
|
+
});
|
|
1045
|
+
const mutationEngine = createMutationEngine({
|
|
1046
|
+
transport: options.transport,
|
|
1047
|
+
cache: cacheStore,
|
|
1048
|
+
queue,
|
|
1049
|
+
reachability,
|
|
1050
|
+
resolvers,
|
|
1051
|
+
events,
|
|
1052
|
+
now,
|
|
1053
|
+
maxAttempts: advanced.maxAttempts,
|
|
1054
|
+
queryEngine,
|
|
1055
|
+
contextProvider: advanced.contextProvider,
|
|
1056
|
+
refreshAuth: advanced.refreshAuth,
|
|
1057
|
+
schedule: advanced.schedule,
|
|
1058
|
+
backoff: advanced.backoff
|
|
1059
|
+
});
|
|
1060
|
+
const maintenance = createMaintenance({
|
|
1061
|
+
cache: cacheStore,
|
|
1062
|
+
events,
|
|
1063
|
+
now,
|
|
1064
|
+
revalidate: (envelope) => queryEngine.query(envelope)
|
|
1065
|
+
});
|
|
1066
|
+
return {
|
|
1067
|
+
...queryEngine,
|
|
1068
|
+
...mutationEngine,
|
|
1069
|
+
maintenance,
|
|
1070
|
+
events,
|
|
1071
|
+
resolvers,
|
|
1072
|
+
localOffline: policy
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
function assertModeCompatibleWithAdvanced(mode, advanced) {
|
|
1076
|
+
if (mode === "offline-capable") {
|
|
1077
|
+
if (advanced.storage === undefined) {
|
|
1078
|
+
throw new Error("localOffline=offline-capable requires advanced.storage; pass a durable StoragePort or explicit platform storage adapter.");
|
|
1079
|
+
}
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (advanced.storage !== undefined) {
|
|
1083
|
+
throw new Error(`localOffline=${mode} rejects advanced.storage; this mode must not use durable local persistence.`);
|
|
1084
|
+
}
|
|
1085
|
+
if (advanced.cacheStore !== undefined) {
|
|
1086
|
+
throw new Error(`localOffline=${mode} rejects advanced.cacheStore; this mode owns cache semantics.`);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
function assertNoLegacyLowLevelOptions(options) {
|
|
1090
|
+
const legacy = options;
|
|
1091
|
+
if (legacy.storage !== undefined) {
|
|
1092
|
+
throw new Error("Use advanced.storage instead of top-level storage.");
|
|
1093
|
+
}
|
|
1094
|
+
if (legacy.cacheStore !== undefined) {
|
|
1095
|
+
throw new Error("Use advanced.cacheStore instead of top-level cacheStore.");
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
// src/engine/data-engine-host.ts
|
|
1099
|
+
function readHostEnv() {
|
|
1100
|
+
const proc = globalThis.process;
|
|
1101
|
+
return proc?.env;
|
|
1102
|
+
}
|
|
1103
|
+
function createDataEngineFromEnv(options) {
|
|
1104
|
+
return createDataEngine({
|
|
1105
|
+
...options,
|
|
1106
|
+
env: options.env ?? readHostEnv()
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
// src/engine/no-cache-store.ts
|
|
1110
|
+
function createNoCacheStore() {
|
|
1111
|
+
return {
|
|
1112
|
+
read: async (_key) => {
|
|
1113
|
+
return;
|
|
1114
|
+
},
|
|
1115
|
+
write: async () => {},
|
|
1116
|
+
remove: async () => {},
|
|
1117
|
+
entries: async () => [],
|
|
1118
|
+
invalidate: async (_tags, _now) => []
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
// src/engine/online-only-mutation-engine.ts
|
|
1122
|
+
function offlineDisabledProblem() {
|
|
1123
|
+
return {
|
|
1124
|
+
type: "https://contractspec.io/problems/offline-queue-disabled",
|
|
1125
|
+
title: "Offline mutation queue disabled",
|
|
1126
|
+
status: 503,
|
|
1127
|
+
code: "OFFLINE_QUEUE_DISABLED",
|
|
1128
|
+
category: "policy",
|
|
1129
|
+
severity: "warning",
|
|
1130
|
+
retryable: true,
|
|
1131
|
+
detail: "localOffline is online-only; offline mutations are not queued or replayed."
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
var toEnvelope = (request) => ({
|
|
1135
|
+
operation: request.operation,
|
|
1136
|
+
input: request.input,
|
|
1137
|
+
tags: request.tags
|
|
1138
|
+
});
|
|
1139
|
+
function createOnlineOnlyMutationEngine(options) {
|
|
1140
|
+
const now = options.now ?? (() => Date.now());
|
|
1141
|
+
let seq = 0;
|
|
1142
|
+
async function mutate(request, mutateOptions = {}) {
|
|
1143
|
+
seq += 1;
|
|
1144
|
+
const id = `${now()}-${seq}`;
|
|
1145
|
+
if (!isNetworkAllowed(options.reachability.getSnapshot().signal)) {
|
|
1146
|
+
return {
|
|
1147
|
+
id,
|
|
1148
|
+
status: "disabled-offline",
|
|
1149
|
+
problem: offlineDisabledProblem()
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
const result = await options.transport.execute(toEnvelope(request), mutateOptions.ctx);
|
|
1153
|
+
if (result.ok)
|
|
1154
|
+
return { id, status: "done", data: result.data.data };
|
|
1155
|
+
return { id, status: "failed", problem: result.problem };
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
mutate,
|
|
1159
|
+
drain: async () => {},
|
|
1160
|
+
resume: async () => {},
|
|
1161
|
+
dispose: () => {}
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
export {
|
|
1165
|
+
authExpiredState,
|
|
1166
|
+
backoffDelayMs,
|
|
1167
|
+
computeIsStale,
|
|
1168
|
+
conflictState,
|
|
1169
|
+
createCacheStore,
|
|
1170
|
+
createConflictResolverRegistry,
|
|
1171
|
+
createDataEngine,
|
|
1172
|
+
createDataEngineFromEnv,
|
|
1173
|
+
createEngineEvents,
|
|
1174
|
+
createInflightRegistry,
|
|
1175
|
+
createMaintenance,
|
|
1176
|
+
createMutationEngine,
|
|
1177
|
+
createMutationQueue,
|
|
1178
|
+
createNoCacheStore,
|
|
1179
|
+
createOnlineOnlyMutationEngine,
|
|
1180
|
+
createQueryEngine,
|
|
1181
|
+
errorState,
|
|
1182
|
+
idleState,
|
|
1183
|
+
loadingState,
|
|
1184
|
+
offlineState,
|
|
1185
|
+
readHostEnv,
|
|
1186
|
+
resolveQueuedConflict,
|
|
1187
|
+
restoreSnapshots,
|
|
1188
|
+
successState
|
|
1189
|
+
};
|