@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,422 @@
|
|
|
1
|
+
// src/engine/online-only-data-engine.ts
|
|
2
|
+
import { createQueryKey } from "@lssm/lib.contracts-spec/query/key";
|
|
3
|
+
|
|
4
|
+
// src/local-offline.ts
|
|
5
|
+
var MODES = new Set([
|
|
6
|
+
"online-only",
|
|
7
|
+
"memory-cache",
|
|
8
|
+
"offline-capable"
|
|
9
|
+
]);
|
|
10
|
+
var MODE_ALIASES = {
|
|
11
|
+
"online-only": "online-only",
|
|
12
|
+
online: "online-only",
|
|
13
|
+
disabled: "online-only",
|
|
14
|
+
false: "online-only",
|
|
15
|
+
off: "online-only",
|
|
16
|
+
"memory-cache": "memory-cache",
|
|
17
|
+
memory: "memory-cache",
|
|
18
|
+
cache: "memory-cache",
|
|
19
|
+
"offline-capable": "offline-capable",
|
|
20
|
+
offline: "offline-capable",
|
|
21
|
+
enabled: "offline-capable",
|
|
22
|
+
true: "offline-capable",
|
|
23
|
+
on: "offline-capable"
|
|
24
|
+
};
|
|
25
|
+
function isLocalOfflineMode(value) {
|
|
26
|
+
return typeof value === "string" && MODES.has(value);
|
|
27
|
+
}
|
|
28
|
+
function resolveLocalOfflinePolicy(options = {}) {
|
|
29
|
+
const fallback = options.defaultMode === undefined ? {
|
|
30
|
+
mode: "online-only",
|
|
31
|
+
source: "default",
|
|
32
|
+
reason: "No local/offline policy input was supplied."
|
|
33
|
+
} : normalizePolicy(options.defaultMode, "explicit") ?? {
|
|
34
|
+
mode: "online-only",
|
|
35
|
+
source: "explicit"
|
|
36
|
+
};
|
|
37
|
+
const env = normalizePolicy(readEnvValue(options.env), "env", {
|
|
38
|
+
allowUnknown: true
|
|
39
|
+
});
|
|
40
|
+
const flag = normalizePolicy(options.flag, "feature-flag");
|
|
41
|
+
return flag ?? env ?? fallback;
|
|
42
|
+
}
|
|
43
|
+
function readEnvValue(input) {
|
|
44
|
+
if (typeof input !== "object" || input === null)
|
|
45
|
+
return input;
|
|
46
|
+
if ("CONTRACTSPEC_LOCAL_OFFLINE" in input) {
|
|
47
|
+
return input.CONTRACTSPEC_LOCAL_OFFLINE;
|
|
48
|
+
}
|
|
49
|
+
if ("mode" in input)
|
|
50
|
+
return input;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
function normalizePolicy(input, source, options = {}) {
|
|
54
|
+
if (input === undefined || input === null || input === "")
|
|
55
|
+
return;
|
|
56
|
+
if (typeof input === "boolean") {
|
|
57
|
+
return { mode: input ? "offline-capable" : "online-only", source };
|
|
58
|
+
}
|
|
59
|
+
if (typeof input === "string") {
|
|
60
|
+
const mode = MODE_ALIASES[input.trim().toLowerCase()];
|
|
61
|
+
if (mode !== undefined)
|
|
62
|
+
return { mode, source };
|
|
63
|
+
if (options.allowUnknown)
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (typeof input === "object" && input !== null && "mode" in input) {
|
|
67
|
+
const policy = input;
|
|
68
|
+
const mode = typeof policy.mode === "string" ? MODE_ALIASES[policy.mode.trim().toLowerCase()] : undefined;
|
|
69
|
+
if (mode !== undefined) {
|
|
70
|
+
return {
|
|
71
|
+
mode,
|
|
72
|
+
source,
|
|
73
|
+
...typeof policy.reason === "string" ? { reason: policy.reason } : {}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (options.allowUnknown)
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Unsupported localOffline policy value: ${String(input)}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/reachability.ts
|
|
83
|
+
function isNetworkAllowed(signal) {
|
|
84
|
+
return signal !== "offline";
|
|
85
|
+
}
|
|
86
|
+
function createStaticReachabilitySource(signal = "online") {
|
|
87
|
+
const snapshot = { signal, since: Date.now() };
|
|
88
|
+
return {
|
|
89
|
+
getSnapshot: () => snapshot,
|
|
90
|
+
subscribe: () => () => {}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
var reachabilityPortDoc = {
|
|
94
|
+
id: "docs.contracts-runtime-core.reachability",
|
|
95
|
+
title: "Reachability input port (tri-state)",
|
|
96
|
+
summary: "Engine-owned reachability input port; integrations feed it, the engine never imports them.",
|
|
97
|
+
kind: "reference",
|
|
98
|
+
visibility: "internal",
|
|
99
|
+
route: "/docs/contracts-runtime-core/reachability",
|
|
100
|
+
domain: "contracts-runtime-core",
|
|
101
|
+
owners: ["@contractspec-core"],
|
|
102
|
+
tags: ["contracts-runtime-core", "reachability", "offline", "port"],
|
|
103
|
+
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."
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/engine/conflict-registry.ts
|
|
107
|
+
function fieldMerge(ctx) {
|
|
108
|
+
const server = ctx.serverData;
|
|
109
|
+
const local = ctx.localData;
|
|
110
|
+
const fields = ctx.mergeFields ?? [];
|
|
111
|
+
if (typeof server !== "object" || server === null || typeof local !== "object" || local === null) {
|
|
112
|
+
return { outcome: "surface" };
|
|
113
|
+
}
|
|
114
|
+
const merged = {
|
|
115
|
+
...server
|
|
116
|
+
};
|
|
117
|
+
const localRecord = local;
|
|
118
|
+
for (const field of fields) {
|
|
119
|
+
if (field in localRecord)
|
|
120
|
+
merged[field] = localRecord[field];
|
|
121
|
+
}
|
|
122
|
+
return { outcome: "merged", data: merged };
|
|
123
|
+
}
|
|
124
|
+
function createConflictResolverRegistry() {
|
|
125
|
+
const resolvers = new Map;
|
|
126
|
+
return {
|
|
127
|
+
register(key, resolver) {
|
|
128
|
+
resolvers.set(key, resolver);
|
|
129
|
+
},
|
|
130
|
+
has(key) {
|
|
131
|
+
return resolvers.has(key);
|
|
132
|
+
},
|
|
133
|
+
async resolve(ctx) {
|
|
134
|
+
switch (ctx.strategy) {
|
|
135
|
+
case "server-wins":
|
|
136
|
+
return { outcome: "use-server", data: ctx.serverData };
|
|
137
|
+
case "client-wins":
|
|
138
|
+
return { outcome: "use-client", data: ctx.localData };
|
|
139
|
+
case "field-merge":
|
|
140
|
+
return fieldMerge(ctx);
|
|
141
|
+
case "surface":
|
|
142
|
+
return { outcome: "surface" };
|
|
143
|
+
default: {
|
|
144
|
+
const key = ctx.policy.resolver;
|
|
145
|
+
const resolver = key === undefined ? undefined : resolvers.get(key);
|
|
146
|
+
if (resolver === undefined)
|
|
147
|
+
return { outcome: "surface" };
|
|
148
|
+
return resolver(ctx);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/engine/events.ts
|
|
156
|
+
function createEngineEvents() {
|
|
157
|
+
const listeners = new Set;
|
|
158
|
+
return {
|
|
159
|
+
emit(event) {
|
|
160
|
+
for (const listener of listeners)
|
|
161
|
+
listener(event);
|
|
162
|
+
},
|
|
163
|
+
subscribe(listener) {
|
|
164
|
+
listeners.add(listener);
|
|
165
|
+
return () => {
|
|
166
|
+
listeners.delete(listener);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/engine/state.ts
|
|
173
|
+
function computeIsStale(entry, now) {
|
|
174
|
+
if (entry?.staleAt === undefined)
|
|
175
|
+
return false;
|
|
176
|
+
return now >= entry.staleAt;
|
|
177
|
+
}
|
|
178
|
+
function idleState() {
|
|
179
|
+
return {
|
|
180
|
+
status: "idle",
|
|
181
|
+
fetchStatus: "idle",
|
|
182
|
+
cacheStatus: "empty",
|
|
183
|
+
isStale: false,
|
|
184
|
+
isOffline: false
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function loadingState(entry, now) {
|
|
188
|
+
if (entry === undefined) {
|
|
189
|
+
return {
|
|
190
|
+
status: "loading",
|
|
191
|
+
fetchStatus: "fetching",
|
|
192
|
+
cacheStatus: "empty",
|
|
193
|
+
isStale: false,
|
|
194
|
+
isOffline: false
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const isStale = computeIsStale(entry, now);
|
|
198
|
+
return {
|
|
199
|
+
status: "success",
|
|
200
|
+
fetchStatus: "fetching",
|
|
201
|
+
data: entry.data,
|
|
202
|
+
cacheStatus: isStale ? "stale" : entry.status,
|
|
203
|
+
isStale,
|
|
204
|
+
isOffline: false,
|
|
205
|
+
versionToken: entry.versionToken,
|
|
206
|
+
dataUpdatedAt: entry.storedAt,
|
|
207
|
+
...entry.pageInfo === undefined ? {} : { pageInfo: entry.pageInfo }
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function successState(entry, now) {
|
|
211
|
+
const isStale = computeIsStale(entry, now);
|
|
212
|
+
return {
|
|
213
|
+
status: "success",
|
|
214
|
+
fetchStatus: "idle",
|
|
215
|
+
data: entry.data,
|
|
216
|
+
cacheStatus: isStale ? "stale" : entry.status,
|
|
217
|
+
isStale,
|
|
218
|
+
isOffline: false,
|
|
219
|
+
versionToken: entry.versionToken,
|
|
220
|
+
dataUpdatedAt: entry.storedAt,
|
|
221
|
+
...entry.pageInfo === undefined ? {} : { pageInfo: entry.pageInfo }
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function errorState(problem, entry, now) {
|
|
225
|
+
return {
|
|
226
|
+
status: "error",
|
|
227
|
+
fetchStatus: "idle",
|
|
228
|
+
data: entry?.data,
|
|
229
|
+
error: problem,
|
|
230
|
+
cacheStatus: entry === undefined ? "empty" : entry.status,
|
|
231
|
+
isStale: computeIsStale(entry, now),
|
|
232
|
+
isOffline: false,
|
|
233
|
+
versionToken: entry?.versionToken,
|
|
234
|
+
dataUpdatedAt: entry?.storedAt,
|
|
235
|
+
errorUpdatedAt: now
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function authExpiredState(problem, entry, now) {
|
|
239
|
+
return {
|
|
240
|
+
status: "auth-expired",
|
|
241
|
+
fetchStatus: "idle",
|
|
242
|
+
data: entry?.data,
|
|
243
|
+
error: problem,
|
|
244
|
+
cacheStatus: entry === undefined ? "empty" : entry.status,
|
|
245
|
+
isStale: computeIsStale(entry, now),
|
|
246
|
+
isOffline: false,
|
|
247
|
+
versionToken: entry?.versionToken,
|
|
248
|
+
dataUpdatedAt: entry?.storedAt,
|
|
249
|
+
errorUpdatedAt: now
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function conflictState(entry, conflict, now) {
|
|
253
|
+
return {
|
|
254
|
+
status: entry === undefined ? "idle" : "success",
|
|
255
|
+
fetchStatus: "idle",
|
|
256
|
+
data: entry?.data,
|
|
257
|
+
cacheStatus: "conflict",
|
|
258
|
+
isStale: computeIsStale(entry, now),
|
|
259
|
+
isOffline: false,
|
|
260
|
+
conflict,
|
|
261
|
+
versionToken: conflict.versionToken ?? entry?.versionToken,
|
|
262
|
+
dataUpdatedAt: entry?.storedAt
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function offlineState(entry, now) {
|
|
266
|
+
return {
|
|
267
|
+
status: entry === undefined ? "idle" : "success",
|
|
268
|
+
fetchStatus: "idle",
|
|
269
|
+
data: entry?.data,
|
|
270
|
+
cacheStatus: "offline",
|
|
271
|
+
isStale: computeIsStale(entry, now),
|
|
272
|
+
isOffline: true,
|
|
273
|
+
versionToken: entry?.versionToken,
|
|
274
|
+
dataUpdatedAt: entry?.storedAt,
|
|
275
|
+
...entry?.pageInfo === undefined ? {} : { pageInfo: entry.pageInfo }
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/engine/online-only-data-engine.ts
|
|
280
|
+
function createOnlineOnlyDataEngine(options, resolvedPolicy) {
|
|
281
|
+
const { transport } = options;
|
|
282
|
+
const advanced = options.advanced ?? {};
|
|
283
|
+
const now = advanced.now ?? (() => Date.now());
|
|
284
|
+
const reachability = advanced.reachability ?? createStaticReachabilitySource("online");
|
|
285
|
+
const events = advanced.events ?? createEngineEvents();
|
|
286
|
+
const resolvers = advanced.resolvers ?? createConflictResolverRegistry();
|
|
287
|
+
const localOffline = resolvedPolicy ?? resolveLocalOfflinePolicy({
|
|
288
|
+
defaultMode: options.localOffline,
|
|
289
|
+
env: options.env,
|
|
290
|
+
flag: options.flag
|
|
291
|
+
});
|
|
292
|
+
const states = new Map;
|
|
293
|
+
const listeners = new Map;
|
|
294
|
+
let seq = 0;
|
|
295
|
+
function emit(key, state) {
|
|
296
|
+
states.set(key, state);
|
|
297
|
+
for (const listener of listeners.get(key) ?? [])
|
|
298
|
+
listener(state);
|
|
299
|
+
}
|
|
300
|
+
const queryEngine = {
|
|
301
|
+
async query(envelope, ctx) {
|
|
302
|
+
const key = createQueryKey(envelope);
|
|
303
|
+
const signal = reachability.getSnapshot().signal;
|
|
304
|
+
if (!isNetworkAllowed(signal)) {
|
|
305
|
+
const state = offlineState(undefined, now());
|
|
306
|
+
emit(key, state);
|
|
307
|
+
return state;
|
|
308
|
+
}
|
|
309
|
+
emit(key, loadingState(undefined, now()));
|
|
310
|
+
const startedAt = now();
|
|
311
|
+
const result = await transport.execute(envelope, ctx);
|
|
312
|
+
const settledAt = now();
|
|
313
|
+
if (!result.ok) {
|
|
314
|
+
events.emit({
|
|
315
|
+
type: "fetch-error",
|
|
316
|
+
key,
|
|
317
|
+
code: result.code,
|
|
318
|
+
latencyMs: settledAt - startedAt
|
|
319
|
+
});
|
|
320
|
+
const state = result.problem.category === "auth" ? authExpiredState(result.problem, undefined, settledAt) : errorState(result.problem, undefined, settledAt);
|
|
321
|
+
emit(key, state);
|
|
322
|
+
return state;
|
|
323
|
+
}
|
|
324
|
+
events.emit({
|
|
325
|
+
type: "fetch-success",
|
|
326
|
+
key,
|
|
327
|
+
latencyMs: settledAt - startedAt
|
|
328
|
+
});
|
|
329
|
+
const entry = {
|
|
330
|
+
key,
|
|
331
|
+
data: result.data.data,
|
|
332
|
+
versionToken: result.data.versionToken ?? result.versionToken,
|
|
333
|
+
status: signal === "restricted" ? "warm" : "fresh",
|
|
334
|
+
tags: [],
|
|
335
|
+
storedAt: settledAt,
|
|
336
|
+
...result.data.pageInfo === undefined ? {} : { pageInfo: result.data.pageInfo }
|
|
337
|
+
};
|
|
338
|
+
const state = successState(entry, settledAt);
|
|
339
|
+
emit(key, state);
|
|
340
|
+
return state;
|
|
341
|
+
},
|
|
342
|
+
getState(envelope) {
|
|
343
|
+
return states.get(createQueryKey(envelope)) ?? idleState();
|
|
344
|
+
},
|
|
345
|
+
subscribe(envelope, listener) {
|
|
346
|
+
const key = createQueryKey(envelope);
|
|
347
|
+
let set = listeners.get(key);
|
|
348
|
+
if (set === undefined) {
|
|
349
|
+
set = new Set;
|
|
350
|
+
listeners.set(key, set);
|
|
351
|
+
}
|
|
352
|
+
set.add(listener);
|
|
353
|
+
return () => set?.delete(listener);
|
|
354
|
+
},
|
|
355
|
+
async invalidateTags() {
|
|
356
|
+
return [];
|
|
357
|
+
},
|
|
358
|
+
async refresh(key) {
|
|
359
|
+
const state = states.get(key) ?? idleState();
|
|
360
|
+
emit(key, state);
|
|
361
|
+
return state;
|
|
362
|
+
},
|
|
363
|
+
async emitConflict(key, conflict) {
|
|
364
|
+
const state = conflictState(undefined, conflict, now());
|
|
365
|
+
emit(key, state);
|
|
366
|
+
return state;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const mutationEngine = {
|
|
370
|
+
async mutate(request, mutateOptions = {}) {
|
|
371
|
+
seq += 1;
|
|
372
|
+
const id = `${now()}-${seq}`;
|
|
373
|
+
if (!isNetworkAllowed(reachability.getSnapshot().signal)) {
|
|
374
|
+
return {
|
|
375
|
+
id,
|
|
376
|
+
status: "disabled-offline",
|
|
377
|
+
problem: offlineQueueDisabledProblem()
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const result = await transport.execute({
|
|
381
|
+
operation: request.operation,
|
|
382
|
+
input: request.input,
|
|
383
|
+
tags: request.tags
|
|
384
|
+
}, mutateOptions.ctx ?? advanced.contextProvider?.());
|
|
385
|
+
if (result.ok)
|
|
386
|
+
return { id, status: "done", data: result.data.data };
|
|
387
|
+
if (result.problem.category === "auth") {
|
|
388
|
+
return { id, status: "paused-auth", problem: result.problem };
|
|
389
|
+
}
|
|
390
|
+
return { id, status: "failed", problem: result.problem };
|
|
391
|
+
},
|
|
392
|
+
async drain() {},
|
|
393
|
+
async resume() {},
|
|
394
|
+
dispose() {}
|
|
395
|
+
};
|
|
396
|
+
return {
|
|
397
|
+
...queryEngine,
|
|
398
|
+
...mutationEngine,
|
|
399
|
+
maintenance: {
|
|
400
|
+
collectGarbage: async () => 0,
|
|
401
|
+
revalidateStale: async () => 0
|
|
402
|
+
},
|
|
403
|
+
events,
|
|
404
|
+
resolvers,
|
|
405
|
+
localOffline
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function offlineQueueDisabledProblem() {
|
|
409
|
+
return {
|
|
410
|
+
type: "about:blank",
|
|
411
|
+
title: "Offline mutation queue disabled",
|
|
412
|
+
status: 409,
|
|
413
|
+
code: "OFFLINE_QUEUE_DISABLED",
|
|
414
|
+
category: "conflict",
|
|
415
|
+
severity: "warning",
|
|
416
|
+
retryable: false,
|
|
417
|
+
detail: "localOffline=online-only does not cache local data or queue offline mutations."
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
export {
|
|
421
|
+
createOnlineOnlyDataEngine
|
|
422
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// src/reachability.ts
|
|
2
|
+
function isNetworkAllowed(signal) {
|
|
3
|
+
return signal !== "offline";
|
|
4
|
+
}
|
|
5
|
+
function createStaticReachabilitySource(signal = "online") {
|
|
6
|
+
const snapshot = { signal, since: Date.now() };
|
|
7
|
+
return {
|
|
8
|
+
getSnapshot: () => snapshot,
|
|
9
|
+
subscribe: () => () => {}
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
var reachabilityPortDoc = {
|
|
13
|
+
id: "docs.contracts-runtime-core.reachability",
|
|
14
|
+
title: "Reachability input port (tri-state)",
|
|
15
|
+
summary: "Engine-owned reachability input port; integrations feed it, the engine never imports them.",
|
|
16
|
+
kind: "reference",
|
|
17
|
+
visibility: "internal",
|
|
18
|
+
route: "/docs/contracts-runtime-core/reachability",
|
|
19
|
+
domain: "contracts-runtime-core",
|
|
20
|
+
owners: ["@contractspec-core"],
|
|
21
|
+
tags: ["contracts-runtime-core", "reachability", "offline", "port"],
|
|
22
|
+
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."
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/engine/online-only-mutation-engine.ts
|
|
26
|
+
function offlineDisabledProblem() {
|
|
27
|
+
return {
|
|
28
|
+
type: "https://contractspec.io/problems/offline-queue-disabled",
|
|
29
|
+
title: "Offline mutation queue disabled",
|
|
30
|
+
status: 503,
|
|
31
|
+
code: "OFFLINE_QUEUE_DISABLED",
|
|
32
|
+
category: "policy",
|
|
33
|
+
severity: "warning",
|
|
34
|
+
retryable: true,
|
|
35
|
+
detail: "localOffline is online-only; offline mutations are not queued or replayed."
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
var toEnvelope = (request) => ({
|
|
39
|
+
operation: request.operation,
|
|
40
|
+
input: request.input,
|
|
41
|
+
tags: request.tags
|
|
42
|
+
});
|
|
43
|
+
function createOnlineOnlyMutationEngine(options) {
|
|
44
|
+
const now = options.now ?? (() => Date.now());
|
|
45
|
+
let seq = 0;
|
|
46
|
+
async function mutate(request, mutateOptions = {}) {
|
|
47
|
+
seq += 1;
|
|
48
|
+
const id = `${now()}-${seq}`;
|
|
49
|
+
if (!isNetworkAllowed(options.reachability.getSnapshot().signal)) {
|
|
50
|
+
return {
|
|
51
|
+
id,
|
|
52
|
+
status: "disabled-offline",
|
|
53
|
+
problem: offlineDisabledProblem()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const result = await options.transport.execute(toEnvelope(request), mutateOptions.ctx);
|
|
57
|
+
if (result.ok)
|
|
58
|
+
return { id, status: "done", data: result.data.data };
|
|
59
|
+
return { id, status: "failed", problem: result.problem };
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
mutate,
|
|
63
|
+
drain: async () => {},
|
|
64
|
+
resume: async () => {},
|
|
65
|
+
dispose: () => {}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export {
|
|
69
|
+
createOnlineOnlyMutationEngine
|
|
70
|
+
};
|