@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,36 @@
|
|
|
1
|
+
// src/engine/maintenance.ts
|
|
2
|
+
function createMaintenance(deps) {
|
|
3
|
+
return {
|
|
4
|
+
async collectGarbage() {
|
|
5
|
+
const ts = deps.now();
|
|
6
|
+
let evicted = 0;
|
|
7
|
+
for (const entry of await deps.cache.entries()) {
|
|
8
|
+
if (entry.expiresAt !== undefined && ts >= entry.expiresAt) {
|
|
9
|
+
await deps.cache.remove(entry.key);
|
|
10
|
+
evicted += 1;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
deps.events.emit({ type: "gc-collected", evicted });
|
|
14
|
+
return evicted;
|
|
15
|
+
},
|
|
16
|
+
async revalidateStale() {
|
|
17
|
+
const revalidate = deps.revalidate;
|
|
18
|
+
if (revalidate === undefined)
|
|
19
|
+
return 0;
|
|
20
|
+
const ts = deps.now();
|
|
21
|
+
let count = 0;
|
|
22
|
+
for (const entry of await deps.cache.entries()) {
|
|
23
|
+
const stale = entry.staleAt !== undefined && ts >= entry.staleAt;
|
|
24
|
+
if (!stale || entry.envelope === undefined)
|
|
25
|
+
continue;
|
|
26
|
+
await revalidate(entry.envelope);
|
|
27
|
+
deps.events.emit({ type: "revalidated", key: entry.key });
|
|
28
|
+
count += 1;
|
|
29
|
+
}
|
|
30
|
+
return count;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
createMaintenance
|
|
36
|
+
};
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// src/engine/mutation-engine.ts
|
|
2
|
+
import {
|
|
3
|
+
mergeSafeEconomicEvidenceRefIds,
|
|
4
|
+
parseSafeEconomicEvidenceRefId
|
|
5
|
+
} from "@lssm/lib.contracts-spec/operations";
|
|
6
|
+
import { defaultConflictPolicy as defaultConflictPolicy2 } from "@lssm/lib.contracts-spec/query/conflict-policy";
|
|
7
|
+
|
|
8
|
+
// src/reachability.ts
|
|
9
|
+
function isNetworkAllowed(signal) {
|
|
10
|
+
return signal !== "offline";
|
|
11
|
+
}
|
|
12
|
+
function createStaticReachabilitySource(signal = "online") {
|
|
13
|
+
const snapshot = { signal, since: Date.now() };
|
|
14
|
+
return {
|
|
15
|
+
getSnapshot: () => snapshot,
|
|
16
|
+
subscribe: () => () => {}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
var reachabilityPortDoc = {
|
|
20
|
+
id: "docs.contracts-runtime-core.reachability",
|
|
21
|
+
title: "Reachability input port (tri-state)",
|
|
22
|
+
summary: "Engine-owned reachability input port; integrations feed it, the engine never imports them.",
|
|
23
|
+
kind: "reference",
|
|
24
|
+
visibility: "internal",
|
|
25
|
+
route: "/docs/contracts-runtime-core/reachability",
|
|
26
|
+
domain: "contracts-runtime-core",
|
|
27
|
+
owners: ["@contractspec-core"],
|
|
28
|
+
tags: ["contracts-runtime-core", "reachability", "offline", "port"],
|
|
29
|
+
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."
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/engine/backoff.ts
|
|
33
|
+
var DEFAULT_BASE_MS = 200;
|
|
34
|
+
var DEFAULT_FACTOR = 2;
|
|
35
|
+
var DEFAULT_MAX_MS = 30000;
|
|
36
|
+
function backoffDelayMs(attempt, options = {}) {
|
|
37
|
+
const base = options.baseMs ?? DEFAULT_BASE_MS;
|
|
38
|
+
const factor = options.factor ?? DEFAULT_FACTOR;
|
|
39
|
+
const max = options.maxMs ?? DEFAULT_MAX_MS;
|
|
40
|
+
const jitter = options.jitter ?? Math.random;
|
|
41
|
+
const exponent = Math.max(0, attempt - 1);
|
|
42
|
+
const raw = Math.min(max, base * factor ** exponent);
|
|
43
|
+
return Math.round(raw * (0.5 + jitter() * 0.5));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/engine/conflict-resolution.ts
|
|
47
|
+
import { defaultConflictPolicy } from "@lssm/lib.contracts-spec/query/conflict-policy";
|
|
48
|
+
async function restoreSnapshots(snapshots, cache) {
|
|
49
|
+
for (const snapshot of snapshots) {
|
|
50
|
+
if (snapshot.previous === undefined)
|
|
51
|
+
await cache.remove(snapshot.key);
|
|
52
|
+
else
|
|
53
|
+
await cache.write(snapshot.previous);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function resolveQueuedConflict(entry, problem, deps) {
|
|
57
|
+
const policy = entry.conflictPolicy ?? defaultConflictPolicy();
|
|
58
|
+
const primaryKey = entry.rollback[0]?.key;
|
|
59
|
+
const localEntry = primaryKey === undefined ? undefined : await deps.cache.read(primaryKey);
|
|
60
|
+
const serverData = problem.metadata?.serverData;
|
|
61
|
+
const resolution = await deps.resolvers.resolve({
|
|
62
|
+
strategy: policy.strategy,
|
|
63
|
+
policy,
|
|
64
|
+
localData: localEntry?.data,
|
|
65
|
+
serverData,
|
|
66
|
+
localVersion: entry.versionToken,
|
|
67
|
+
serverVersion: problem.versionToken,
|
|
68
|
+
mergeFields: policy.mergeFields
|
|
69
|
+
});
|
|
70
|
+
deps.events.emit({
|
|
71
|
+
type: "mutation-conflict",
|
|
72
|
+
id: entry.id,
|
|
73
|
+
strategy: policy.strategy,
|
|
74
|
+
outcome: resolution.outcome
|
|
75
|
+
});
|
|
76
|
+
switch (resolution.outcome) {
|
|
77
|
+
case "use-server": {
|
|
78
|
+
await restoreSnapshots(entry.rollback, deps.cache);
|
|
79
|
+
await deps.cache.invalidate(entry.tags, deps.now());
|
|
80
|
+
for (const snapshot of entry.rollback) {
|
|
81
|
+
await deps.queryEngine?.refresh(snapshot.key);
|
|
82
|
+
}
|
|
83
|
+
return "resolved";
|
|
84
|
+
}
|
|
85
|
+
case "use-client":
|
|
86
|
+
await deps.queue.update({
|
|
87
|
+
...entry,
|
|
88
|
+
versionToken: problem.versionToken,
|
|
89
|
+
status: "pending"
|
|
90
|
+
});
|
|
91
|
+
return "re-enqueued";
|
|
92
|
+
case "merged": {
|
|
93
|
+
if (primaryKey !== undefined && localEntry !== undefined) {
|
|
94
|
+
await deps.cache.write({
|
|
95
|
+
...localEntry,
|
|
96
|
+
data: resolution.data,
|
|
97
|
+
versionToken: problem.versionToken,
|
|
98
|
+
status: "warm",
|
|
99
|
+
storedAt: deps.now()
|
|
100
|
+
});
|
|
101
|
+
await deps.queryEngine?.refresh(primaryKey);
|
|
102
|
+
}
|
|
103
|
+
await deps.queue.update({
|
|
104
|
+
...entry,
|
|
105
|
+
versionToken: problem.versionToken,
|
|
106
|
+
status: "pending"
|
|
107
|
+
});
|
|
108
|
+
return "re-enqueued";
|
|
109
|
+
}
|
|
110
|
+
default: {
|
|
111
|
+
if (primaryKey !== undefined) {
|
|
112
|
+
await deps.queryEngine?.emitConflict(primaryKey, {
|
|
113
|
+
versionToken: problem.versionToken,
|
|
114
|
+
serverData,
|
|
115
|
+
clientData: localEntry?.data,
|
|
116
|
+
strategy: policy.strategy
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return "resolved";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/engine/mutation-engine.ts
|
|
125
|
+
var DEFAULT_MAX_ATTEMPTS = 5;
|
|
126
|
+
function mergeEconomicEvidenceRefs(...groups) {
|
|
127
|
+
return mergeSafeEconomicEvidenceRefIds(...groups);
|
|
128
|
+
}
|
|
129
|
+
function toRuntimeEconomicEvidenceRef(ref) {
|
|
130
|
+
const safeRef = parseSafeEconomicEvidenceRefId(ref);
|
|
131
|
+
if (!safeRef) {
|
|
132
|
+
throw new Error("EconomicEvidenceInvalidRef: unsafe or invalid economic evidence ref");
|
|
133
|
+
}
|
|
134
|
+
return safeRef;
|
|
135
|
+
}
|
|
136
|
+
function createMutationEngine(options) {
|
|
137
|
+
const { transport, cache, queue, reachability, resolvers, events } = options;
|
|
138
|
+
const now = options.now ?? (() => Date.now());
|
|
139
|
+
const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
140
|
+
const deps = {
|
|
141
|
+
cache,
|
|
142
|
+
queue,
|
|
143
|
+
resolvers,
|
|
144
|
+
events,
|
|
145
|
+
now,
|
|
146
|
+
queryEngine: options.queryEngine
|
|
147
|
+
};
|
|
148
|
+
let paused = false;
|
|
149
|
+
let seq = 0;
|
|
150
|
+
let draining = null;
|
|
151
|
+
const outcomes = new Map;
|
|
152
|
+
const toEnvelope = (entry, economicEvidenceRefs = entry.economicEvidenceRefs) => ({
|
|
153
|
+
operation: entry.operation,
|
|
154
|
+
input: entry.input,
|
|
155
|
+
tags: entry.tags,
|
|
156
|
+
economicEvidenceRefs
|
|
157
|
+
});
|
|
158
|
+
async function applyOptimistic(writes) {
|
|
159
|
+
const snapshots = [];
|
|
160
|
+
for (const write of writes) {
|
|
161
|
+
const previous = await cache.read(write.key);
|
|
162
|
+
snapshots.push({ key: write.key, previous });
|
|
163
|
+
await cache.write({
|
|
164
|
+
key: write.key,
|
|
165
|
+
data: write.data,
|
|
166
|
+
versionToken: write.versionToken ?? previous?.versionToken,
|
|
167
|
+
status: "warm",
|
|
168
|
+
tags: previous?.tags ?? [],
|
|
169
|
+
storedAt: now(),
|
|
170
|
+
staleAt: previous?.staleAt,
|
|
171
|
+
expiresAt: previous?.expiresAt
|
|
172
|
+
});
|
|
173
|
+
await options.queryEngine?.refresh(write.key);
|
|
174
|
+
}
|
|
175
|
+
return snapshots;
|
|
176
|
+
}
|
|
177
|
+
async function commitSuccess(entry, result) {
|
|
178
|
+
for (const snapshot of entry.rollback) {
|
|
179
|
+
const current = await cache.read(snapshot.key);
|
|
180
|
+
if (current !== undefined) {
|
|
181
|
+
await cache.write({
|
|
182
|
+
...current,
|
|
183
|
+
versionToken: result.versionToken ?? current.versionToken,
|
|
184
|
+
status: "stale",
|
|
185
|
+
staleAt: now()
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
await options.queryEngine?.refresh(snapshot.key);
|
|
189
|
+
}
|
|
190
|
+
await cache.invalidate(entry.tags, now());
|
|
191
|
+
}
|
|
192
|
+
async function replayEntry(entry) {
|
|
193
|
+
let safeEntryEconomicEvidenceRefs;
|
|
194
|
+
try {
|
|
195
|
+
safeEntryEconomicEvidenceRefs = mergeEconomicEvidenceRefs(entry.economicEvidenceRefs);
|
|
196
|
+
} catch {
|
|
197
|
+
await restoreSnapshots(entry.rollback, cache);
|
|
198
|
+
for (const snapshot of entry.rollback) {
|
|
199
|
+
await options.queryEngine?.refresh(snapshot.key);
|
|
200
|
+
}
|
|
201
|
+
await queue.remove(entry.id);
|
|
202
|
+
events.emit({
|
|
203
|
+
type: "mutation-failed",
|
|
204
|
+
id: entry.id,
|
|
205
|
+
code: "ECONOMIC_EVIDENCE_INVALID_REF"
|
|
206
|
+
});
|
|
207
|
+
outcomes.set(entry.id, {
|
|
208
|
+
id: entry.id,
|
|
209
|
+
status: "failed",
|
|
210
|
+
problem: {
|
|
211
|
+
type: "about:blank",
|
|
212
|
+
title: "Economic evidence ref rejected",
|
|
213
|
+
status: 422,
|
|
214
|
+
code: "ECONOMIC_EVIDENCE_INVALID_REF",
|
|
215
|
+
category: "validation",
|
|
216
|
+
severity: "error",
|
|
217
|
+
retryable: false
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
const attempts = entry.attempts + 1;
|
|
223
|
+
await queue.update({ ...entry, status: "in-flight", attempts });
|
|
224
|
+
const result = await transport.execute(toEnvelope(entry, safeEntryEconomicEvidenceRefs), options.contextProvider?.());
|
|
225
|
+
if (result.ok) {
|
|
226
|
+
const economicEvidenceRefs = mergeEconomicEvidenceRefs(safeEntryEconomicEvidenceRefs, result.data.economicEvidenceRefs);
|
|
227
|
+
await commitSuccess(entry, result.data);
|
|
228
|
+
await queue.remove(entry.id);
|
|
229
|
+
events.emit({
|
|
230
|
+
type: "mutation-replayed",
|
|
231
|
+
id: entry.id,
|
|
232
|
+
queueDepth: await queue.depth()
|
|
233
|
+
});
|
|
234
|
+
if (economicEvidenceRefs.length > 0) {
|
|
235
|
+
events.emit({
|
|
236
|
+
type: "economic-evidence-attached",
|
|
237
|
+
operationKey: entry.operation.key,
|
|
238
|
+
evidenceRefs: economicEvidenceRefs.map(toRuntimeEconomicEvidenceRef)
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
outcomes.set(entry.id, {
|
|
242
|
+
id: entry.id,
|
|
243
|
+
status: "done",
|
|
244
|
+
data: result.data.data,
|
|
245
|
+
economicEvidenceRefs
|
|
246
|
+
});
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
const { problem } = result;
|
|
250
|
+
if (problem.category === "auth") {
|
|
251
|
+
paused = true;
|
|
252
|
+
await queue.update({ ...entry, status: "paused-auth", attempts });
|
|
253
|
+
events.emit({ type: "auth-expired", id: entry.id });
|
|
254
|
+
outcomes.set(entry.id, { id: entry.id, status: "paused-auth", problem });
|
|
255
|
+
if (options.refreshAuth !== undefined && await options.refreshAuth()) {
|
|
256
|
+
paused = false;
|
|
257
|
+
await queue.update({ ...entry, status: "pending", attempts });
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
if (problem.category === "conflict") {
|
|
263
|
+
const disposition = await resolveQueuedConflict(entry, problem, deps);
|
|
264
|
+
if (disposition === "resolved") {
|
|
265
|
+
await queue.remove(entry.id);
|
|
266
|
+
outcomes.set(entry.id, { id: entry.id, status: "done", problem });
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
if (problem.retryable && attempts < maxAttempts) {
|
|
271
|
+
const delayMs = backoffDelayMs(attempts, options.backoff);
|
|
272
|
+
await queue.update({ ...entry, status: "failed", attempts });
|
|
273
|
+
events.emit({
|
|
274
|
+
type: "mutation-retry",
|
|
275
|
+
id: entry.id,
|
|
276
|
+
attempt: attempts,
|
|
277
|
+
delayMs
|
|
278
|
+
});
|
|
279
|
+
outcomes.set(entry.id, { id: entry.id, status: "failed", problem });
|
|
280
|
+
options.schedule?.(() => {
|
|
281
|
+
drain();
|
|
282
|
+
}, delayMs);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
await restoreSnapshots(entry.rollback, cache);
|
|
286
|
+
for (const snapshot of entry.rollback) {
|
|
287
|
+
await options.queryEngine?.refresh(snapshot.key);
|
|
288
|
+
}
|
|
289
|
+
await queue.remove(entry.id);
|
|
290
|
+
events.emit({ type: "mutation-failed", id: entry.id, code: result.code });
|
|
291
|
+
outcomes.set(entry.id, { id: entry.id, status: "failed", problem });
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
async function runDrain() {
|
|
295
|
+
if (paused)
|
|
296
|
+
return;
|
|
297
|
+
if (!isNetworkAllowed(reachability.getSnapshot().signal))
|
|
298
|
+
return;
|
|
299
|
+
for (const entry of await queue.list()) {
|
|
300
|
+
if (entry.status === "paused-auth")
|
|
301
|
+
continue;
|
|
302
|
+
if (!await replayEntry(entry))
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
events.emit({ type: "queue-drained", remaining: await queue.depth() });
|
|
306
|
+
}
|
|
307
|
+
function drain() {
|
|
308
|
+
if (draining !== null)
|
|
309
|
+
return draining;
|
|
310
|
+
draining = runDrain().finally(() => {
|
|
311
|
+
draining = null;
|
|
312
|
+
});
|
|
313
|
+
return draining;
|
|
314
|
+
}
|
|
315
|
+
async function mutate(request, mutateOptions = {}) {
|
|
316
|
+
mergeEconomicEvidenceRefs(request.economicEvidenceRefs);
|
|
317
|
+
const enqueuedAt = now();
|
|
318
|
+
seq += 1;
|
|
319
|
+
const id = `${enqueuedAt}-${seq}`;
|
|
320
|
+
const rollback = mutateOptions.optimistic ? await applyOptimistic(mutateOptions.optimistic) : [];
|
|
321
|
+
await queue.enqueue({
|
|
322
|
+
...request,
|
|
323
|
+
conflictPolicy: request.conflictPolicy ?? defaultConflictPolicy2(),
|
|
324
|
+
id,
|
|
325
|
+
enqueuedAt,
|
|
326
|
+
attempts: 0,
|
|
327
|
+
status: "pending",
|
|
328
|
+
rollback
|
|
329
|
+
});
|
|
330
|
+
events.emit({
|
|
331
|
+
type: "mutation-enqueued",
|
|
332
|
+
id,
|
|
333
|
+
queueDepth: await queue.depth()
|
|
334
|
+
});
|
|
335
|
+
if (!paused && isNetworkAllowed(reachability.getSnapshot().signal)) {
|
|
336
|
+
await drain();
|
|
337
|
+
}
|
|
338
|
+
return outcomes.get(id) ?? {
|
|
339
|
+
id,
|
|
340
|
+
status: "pending"
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
async function resume() {
|
|
344
|
+
paused = false;
|
|
345
|
+
for (const entry of await queue.list()) {
|
|
346
|
+
if (entry.status === "paused-auth") {
|
|
347
|
+
await queue.update({ ...entry, status: "pending" });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
await drain();
|
|
351
|
+
}
|
|
352
|
+
const unsubscribe = reachability.subscribe((snapshot) => {
|
|
353
|
+
if (!paused && isNetworkAllowed(snapshot.signal))
|
|
354
|
+
drain();
|
|
355
|
+
});
|
|
356
|
+
return { mutate, drain, resume, dispose: unsubscribe };
|
|
357
|
+
}
|
|
358
|
+
export {
|
|
359
|
+
createMutationEngine
|
|
360
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/engine/mutation-queue.ts
|
|
2
|
+
var QUEUE_PREFIX = "queue:";
|
|
3
|
+
function createMutationQueue(storage) {
|
|
4
|
+
const storageKey = (id) => `${QUEUE_PREFIX}${id}`;
|
|
5
|
+
async function readAll() {
|
|
6
|
+
const keys = await storage.keys(QUEUE_PREFIX);
|
|
7
|
+
const entries = [];
|
|
8
|
+
for (const key of keys) {
|
|
9
|
+
const entry = await storage.get(key);
|
|
10
|
+
if (entry !== undefined)
|
|
11
|
+
entries.push(entry);
|
|
12
|
+
}
|
|
13
|
+
entries.sort((a, b) => a.enqueuedAt === b.enqueuedAt ? a.id.localeCompare(b.id) : a.enqueuedAt - b.enqueuedAt);
|
|
14
|
+
return entries;
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
enqueue: (entry) => storage.set(storageKey(entry.id), entry),
|
|
18
|
+
update: (entry) => storage.set(storageKey(entry.id), entry),
|
|
19
|
+
remove: (id) => storage.remove(storageKey(id)),
|
|
20
|
+
list: () => readAll(),
|
|
21
|
+
depth: async () => (await storage.keys(QUEUE_PREFIX)).length
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
createMutationQueue
|
|
26
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/engine/no-cache-store.ts
|
|
2
|
+
function createNoCacheStore() {
|
|
3
|
+
return {
|
|
4
|
+
read: async (_key) => {
|
|
5
|
+
return;
|
|
6
|
+
},
|
|
7
|
+
write: async () => {},
|
|
8
|
+
remove: async () => {},
|
|
9
|
+
entries: async () => [],
|
|
10
|
+
invalidate: async (_tags, _now) => []
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
createNoCacheStore
|
|
15
|
+
};
|