@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,76 @@
|
|
|
1
|
+
import type { ConflictStrategy } from '@lssm/lib.contracts-spec/query/conflict-policy';
|
|
2
|
+
/**
|
|
3
|
+
* Structured engine lifecycle events. This is the observability *seam*: B1b
|
|
4
|
+
* emits them; the observability lib binding + secret redaction land in #7.
|
|
5
|
+
*
|
|
6
|
+
* `key` may embed query input, so the #7 sink is responsible for redaction
|
|
7
|
+
* before any persistence/logging (AC12). These in-memory events carry no
|
|
8
|
+
* payload data — only identifiers, counts, latency, and outcomes.
|
|
9
|
+
*/
|
|
10
|
+
export interface EconomicEvidenceRuntimeRef {
|
|
11
|
+
id: string;
|
|
12
|
+
kind: 'usage' | 'cost' | 'budget' | 'replay' | 'projection';
|
|
13
|
+
source?: string;
|
|
14
|
+
}
|
|
15
|
+
export type ConflictOutcome = 'use-server' | 'use-client' | 'merged' | 'surface';
|
|
16
|
+
export type EngineEvent = {
|
|
17
|
+
type: 'cache-hit';
|
|
18
|
+
key: string;
|
|
19
|
+
} | {
|
|
20
|
+
type: 'cache-miss';
|
|
21
|
+
key: string;
|
|
22
|
+
} | {
|
|
23
|
+
type: 'fetch-success';
|
|
24
|
+
key: string;
|
|
25
|
+
latencyMs: number;
|
|
26
|
+
} | {
|
|
27
|
+
type: 'fetch-error';
|
|
28
|
+
key: string;
|
|
29
|
+
code: string;
|
|
30
|
+
latencyMs: number;
|
|
31
|
+
} | {
|
|
32
|
+
type: 'mutation-enqueued';
|
|
33
|
+
id: string;
|
|
34
|
+
queueDepth: number;
|
|
35
|
+
} | {
|
|
36
|
+
type: 'mutation-replayed';
|
|
37
|
+
id: string;
|
|
38
|
+
queueDepth: number;
|
|
39
|
+
} | {
|
|
40
|
+
type: 'mutation-conflict';
|
|
41
|
+
id: string;
|
|
42
|
+
strategy: ConflictStrategy;
|
|
43
|
+
outcome: ConflictOutcome;
|
|
44
|
+
} | {
|
|
45
|
+
type: 'mutation-retry';
|
|
46
|
+
id: string;
|
|
47
|
+
attempt: number;
|
|
48
|
+
delayMs: number;
|
|
49
|
+
} | {
|
|
50
|
+
type: 'mutation-failed';
|
|
51
|
+
id: string;
|
|
52
|
+
code: string;
|
|
53
|
+
} | {
|
|
54
|
+
type: 'auth-expired';
|
|
55
|
+
id: string;
|
|
56
|
+
} | {
|
|
57
|
+
type: 'queue-drained';
|
|
58
|
+
remaining: number;
|
|
59
|
+
} | {
|
|
60
|
+
type: 'gc-collected';
|
|
61
|
+
evicted: number;
|
|
62
|
+
} | {
|
|
63
|
+
type: 'revalidated';
|
|
64
|
+
key: string;
|
|
65
|
+
} | {
|
|
66
|
+
type: 'economic-evidence-attached';
|
|
67
|
+
operationKey: string;
|
|
68
|
+
evidenceRefs: readonly EconomicEvidenceRuntimeRef[];
|
|
69
|
+
latencyMs?: number;
|
|
70
|
+
};
|
|
71
|
+
export type EngineEventListener = (event: EngineEvent) => void;
|
|
72
|
+
export interface EngineEvents {
|
|
73
|
+
emit(event: EngineEvent): void;
|
|
74
|
+
subscribe(listener: EngineEventListener): () => void;
|
|
75
|
+
}
|
|
76
|
+
export declare function createEngineEvents(): EngineEvents;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { type BackoffOptions, backoffDelayMs } from './backoff.js';
|
|
2
|
+
export { type CacheStore, createCacheStore } from './cache-store.js';
|
|
3
|
+
export { createConflictResolverRegistry } from './conflict-registry.js';
|
|
4
|
+
export { type ConflictDeps, type ConflictDisposition, resolveQueuedConflict, restoreSnapshots, } from './conflict-resolution.js';
|
|
5
|
+
export { createDataEngine, type DataEngine, type DataEngineAdvancedOptions, type DataEngineOptions, } from './data-engine.js';
|
|
6
|
+
export { createDataEngineFromEnv, readHostEnv } from './data-engine-host.js';
|
|
7
|
+
export { createInflightRegistry, type InflightRegistry } from './dedup.js';
|
|
8
|
+
export { type ConflictOutcome, createEngineEvents, type EngineEvent, type EngineEventListener, type EngineEvents, } from './events.js';
|
|
9
|
+
export { createMaintenance, type Maintenance, type MaintenanceDeps, } from './maintenance.js';
|
|
10
|
+
export { createMutationEngine } from './mutation-engine.js';
|
|
11
|
+
export { createMutationQueue, type MutationQueue } from './mutation-queue.js';
|
|
12
|
+
export type { ConflictContext, ConflictOutcomeKind, ConflictResolution, ConflictResolver, ConflictResolverRegistry, MutateOptions, MutationEngine, MutationEngineOptions, MutationQueueEntry, MutationQueueStatus, MutationRequest, MutationResult, OptimisticSnapshot, OptimisticWrite, } from './mutation-types.js';
|
|
13
|
+
export { createNoCacheStore } from './no-cache-store.js';
|
|
14
|
+
export { createOnlineOnlyMutationEngine } from './online-only-mutation-engine.js';
|
|
15
|
+
export { createQueryEngine } from './query-engine.js';
|
|
16
|
+
export { authExpiredState, computeIsStale, conflictState, errorState, idleState, loadingState, offlineState, successState, } from './state.js';
|
|
17
|
+
export type { CacheEntry, QueryEngine, QueryEngineOptions, QueryStateListener, } from './types.js';
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
function W(e,t={}){let n=t.baseMs??200,a=t.factor??2,i=t.maxMs??30000,u=t.jitter??Math.random,r=Math.max(0,e-1),d=Math.min(i,n*a**r);return Math.round(d*(0.5+u()*0.5))}import{invalidationTagMatches as le}from"@lssm/lib.contracts-spec/query/invalidation";var re="cache:";function L(e){let t=new Map,n=(a)=>`${re}${a}`;return{read:(a)=>e.get(n(a)),write:async(a)=>{t.set(a.key,a.tags),await e.set(n(a.key),a)},remove:async(a)=>{t.delete(a),await e.remove(n(a))},entries:async()=>{let a=await e.keys(re),i=[];for(let u of a){let r=await e.get(u);if(r!==void 0)i.push(r)}return i},invalidate:async(a,i)=>{let u=[];for(let[r,d]of t.entries()){if(!d.some((h)=>a.some((E)=>le(h,E))))continue;let T=await e.get(n(r));if(T===void 0)continue;await e.set(n(r),{...T,status:"stale",staleAt:i}),u.push(r)}return u}}}function fe(e){let{serverData:t,localData:n}=e,a=e.mergeFields??[];if(typeof t!=="object"||t===null||typeof n!=="object"||n===null)return{outcome:"surface"};let i={...t},u=n;for(let r of a)if(r in u)i[r]=u[r];return{outcome:"merged",data:i}}function I(){let e=new Map;return{register(t,n){e.set(t,n)},has(t){return e.has(t)},async resolve(t){switch(t.strategy){case"server-wins":return{outcome:"use-server",data:t.serverData};case"client-wins":return{outcome:"use-client",data:t.localData};case"field-merge":return fe(t);case"surface":return{outcome:"surface"};default:{let n=t.policy.resolver,a=n===void 0?void 0:e.get(n);if(a===void 0)return{outcome:"surface"};return a(t)}}}}}import{defaultConflictPolicy as de}from"@lssm/lib.contracts-spec/query/conflict-policy";async function _(e,t){for(let n of e)if(n.previous===void 0)await t.remove(n.key);else await t.write(n.previous)}async function z(e,t,n){let a=e.conflictPolicy??de(),i=e.rollback[0]?.key,u=i===void 0?void 0:await n.cache.read(i),r=t.metadata?.serverData,d=await n.resolvers.resolve({strategy:a.strategy,policy:a,localData:u?.data,serverData:r,localVersion:e.versionToken,serverVersion:t.versionToken,mergeFields:a.mergeFields});switch(n.events.emit({type:"mutation-conflict",id:e.id,strategy:a.strategy,outcome:d.outcome}),d.outcome){case"use-server":{await _(e.rollback,n.cache),await n.cache.invalidate(e.tags,n.now());for(let M of e.rollback)await n.queryEngine?.refresh(M.key);return"resolved"}case"use-client":return await n.queue.update({...e,versionToken:t.versionToken,status:"pending"}),"re-enqueued";case"merged":{if(i!==void 0&&u!==void 0)await n.cache.write({...u,data:d.data,versionToken:t.versionToken,status:"warm",storedAt:n.now()}),await n.queryEngine?.refresh(i);return await n.queue.update({...e,versionToken:t.versionToken,status:"pending"}),"re-enqueued"}default:{if(i!==void 0)await n.queryEngine?.emitConflict(i,{versionToken:t.versionToken,serverData:r,clientData:u?.data,strategy:a.strategy});return"resolved"}}}var pe=new Set(["online-only","memory-cache","offline-capable"]),se={"online-only":"online-only",online:"online-only",disabled:"online-only",false:"online-only",off:"online-only","memory-cache":"memory-cache",memory:"memory-cache",cache:"memory-cache","offline-capable":"offline-capable",offline:"offline-capable",enabled:"offline-capable",true:"offline-capable",on:"offline-capable"};function Le(e){return typeof e==="string"&&pe.has(e)}function K(e={}){let t=e.defaultMode===void 0?{mode:"online-only",source:"default",reason:"No local/offline policy input was supplied."}:G(e.defaultMode,"explicit")??{mode:"online-only",source:"explicit"},n=G(me(e.env),"env",{allowUnknown:!0});return G(e.flag,"feature-flag")??n??t}function me(e){if(typeof e!=="object"||e===null)return e;if("CONTRACTSPEC_LOCAL_OFFLINE"in e)return e.CONTRACTSPEC_LOCAL_OFFLINE;if("mode"in e)return e;return}function G(e,t,n={}){if(e===void 0||e===null||e==="")return;if(typeof e==="boolean")return{mode:e?"offline-capable":"online-only",source:t};if(typeof e==="string"){let a=se[e.trim().toLowerCase()];if(a!==void 0)return{mode:a,source:t};if(n.allowUnknown)return}if(typeof e==="object"&&e!==null&&"mode"in e){let a=e,i=typeof a.mode==="string"?se[a.mode.trim().toLowerCase()]:void 0;if(i!==void 0)return{mode:i,source:t,...typeof a.reason==="string"?{reason:a.reason}:{}};if(n.allowUnknown)return}throw Error(`Unsupported localOffline policy value: ${String(e)}`)}function C(e){return e!=="offline"}function q(e="online"){let t={signal:e,since:Date.now()};return{getSnapshot:()=>t,subscribe:()=>()=>{}}}var _e={id:"docs.contracts-runtime-core.reachability",title:"Reachability input port (tri-state)",summary:"Engine-owned reachability input port; integrations feed it, the engine never imports them.",kind:"reference",visibility:"internal",route:"/docs/contracts-runtime-core/reachability",domain:"contracts-runtime-core",owners:["@contractspec-core"],tags:["contracts-runtime-core","reachability","offline","port"],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/* \u2014 concrete sources (runtime-local networkReachability) are wired via an integration\u2192port adapter (plan \xA72.3/\xA74.2, Architect finding 1). isNetworkAllowed mirrors runtime-local's outboundNetworkAllowed = reachability !== 'offline'. createStaticReachabilitySource('online') is the B1a default and online test fixture."};function X(){let e=new Map;return{get:async(t)=>{let n=e.get(t);return n===void 0?void 0:structuredClone(n)},set:async(t,n)=>{e.set(t,structuredClone(n))},remove:async(t)=>{e.delete(t)},keys:async(t)=>{let n=[...e.keys()];return t===void 0?n:n.filter((a)=>a.startsWith(t))},clear:async()=>{e.clear()}}}function P(){let e=new Set;return{emit(t){for(let n of e)n(t)},subscribe(t){return e.add(t),()=>{e.delete(t)}}}}function H(e){return{async collectGarbage(){let t=e.now(),n=0;for(let a of await e.cache.entries())if(a.expiresAt!==void 0&&t>=a.expiresAt)await e.cache.remove(a.key),n+=1;return e.events.emit({type:"gc-collected",evicted:n}),n},async revalidateStale(){let t=e.revalidate;if(t===void 0)return 0;let n=e.now(),a=0;for(let i of await e.cache.entries()){if(!(i.staleAt!==void 0&&n>=i.staleAt)||i.envelope===void 0)continue;await t(i.envelope),e.events.emit({type:"revalidated",key:i.key}),a+=1}return a}}}import{mergeSafeEconomicEvidenceRefIds as ye,parseSafeEconomicEvidenceRefId as ge}from"@lssm/lib.contracts-spec/operations";import{defaultConflictPolicy as ve}from"@lssm/lib.contracts-spec/query/conflict-policy";var he=5;function J(...e){return ye(...e)}function Ee(e){let t=ge(e);if(!t)throw Error("EconomicEvidenceInvalidRef: unsafe or invalid economic evidence ref");return t}function Y(e){let{transport:t,cache:n,queue:a,reachability:i,resolvers:u,events:r}=e,d=e.now??(()=>Date.now()),M=e.maxAttempts??he,T={cache:n,queue:a,resolvers:u,events:r,now:d,queryEngine:e.queryEngine},h=!1,E=0,S=null,D=new Map,j=(o,l=o.economicEvidenceRefs)=>({operation:o.operation,input:o.input,tags:o.tags,economicEvidenceRefs:l});async function v(o){let l=[];for(let f of o){let g=await n.read(f.key);l.push({key:f.key,previous:g}),await n.write({key:f.key,data:f.data,versionToken:f.versionToken??g?.versionToken,status:"warm",tags:g?.tags??[],storedAt:d(),staleAt:g?.staleAt,expiresAt:g?.expiresAt}),await e.queryEngine?.refresh(f.key)}return l}async function w(o,l){for(let f of o.rollback){let g=await n.read(f.key);if(g!==void 0)await n.write({...g,versionToken:l.versionToken??g.versionToken,status:"stale",staleAt:d()});await e.queryEngine?.refresh(f.key)}await n.invalidate(o.tags,d())}async function m(o){let l;try{l=J(o.economicEvidenceRefs)}catch{await _(o.rollback,n);for(let b of o.rollback)await e.queryEngine?.refresh(b.key);return await a.remove(o.id),r.emit({type:"mutation-failed",id:o.id,code:"ECONOMIC_EVIDENCE_INVALID_REF"}),D.set(o.id,{id:o.id,status:"failed",problem:{type:"about:blank",title:"Economic evidence ref rejected",status:422,code:"ECONOMIC_EVIDENCE_INVALID_REF",category:"validation",severity:"error",retryable:!1}}),!0}let f=o.attempts+1;await a.update({...o,status:"in-flight",attempts:f});let g=await t.execute(j(o,l),e.contextProvider?.());if(g.ok){let b=J(l,g.data.economicEvidenceRefs);if(await w(o,g.data),await a.remove(o.id),r.emit({type:"mutation-replayed",id:o.id,queueDepth:await a.depth()}),b.length>0)r.emit({type:"economic-evidence-attached",operationKey:o.operation.key,evidenceRefs:b.map(Ee)});return D.set(o.id,{id:o.id,status:"done",data:g.data.data,economicEvidenceRefs:b}),!0}let{problem:k}=g;if(k.category==="auth"){if(h=!0,await a.update({...o,status:"paused-auth",attempts:f}),r.emit({type:"auth-expired",id:o.id}),D.set(o.id,{id:o.id,status:"paused-auth",problem:k}),e.refreshAuth!==void 0&&await e.refreshAuth())return h=!1,await a.update({...o,status:"pending",attempts:f}),!0;return!1}if(k.category==="conflict"){if(await z(o,k,T)==="resolved")await a.remove(o.id),D.set(o.id,{id:o.id,status:"done",problem:k});return!0}if(k.retryable&&f<M){let b=W(f,e.backoff);return await a.update({...o,status:"failed",attempts:f}),r.emit({type:"mutation-retry",id:o.id,attempt:f,delayMs:b}),D.set(o.id,{id:o.id,status:"failed",problem:k}),e.schedule?.(()=>{p()},b),!0}await _(o.rollback,n);for(let b of o.rollback)await e.queryEngine?.refresh(b.key);return await a.remove(o.id),r.emit({type:"mutation-failed",id:o.id,code:g.code}),D.set(o.id,{id:o.id,status:"failed",problem:k}),!0}async function c(){if(h)return;if(!C(i.getSnapshot().signal))return;for(let o of await a.list()){if(o.status==="paused-auth")continue;if(!await m(o))return}r.emit({type:"queue-drained",remaining:await a.depth()})}function p(){if(S!==null)return S;return S=c().finally(()=>{S=null}),S}async function s(o,l={}){J(o.economicEvidenceRefs);let f=d();E+=1;let g=`${f}-${E}`,k=l.optimistic?await v(l.optimistic):[];if(await a.enqueue({...o,conflictPolicy:o.conflictPolicy??ve(),id:g,enqueuedAt:f,attempts:0,status:"pending",rollback:k}),r.emit({type:"mutation-enqueued",id:g,queueDepth:await a.depth()}),!h&&C(i.getSnapshot().signal))await p();return D.get(g)??{id:g,status:"pending"}}async function y(){h=!1;for(let o of await a.list())if(o.status==="paused-auth")await a.update({...o,status:"pending"});await p()}let O=i.subscribe((o)=>{if(!h&&C(o.signal))p()});return{mutate:s,drain:p,resume:y,dispose:O}}function Z(e){let t=(a)=>`queue:${a}`;async function n(){let a=await e.keys("queue:"),i=[];for(let u of a){let r=await e.get(u);if(r!==void 0)i.push(r)}return i.sort((u,r)=>u.enqueuedAt===r.enqueuedAt?u.id.localeCompare(r.id):u.enqueuedAt-r.enqueuedAt),i}return{enqueue:(a)=>e.set(t(a.id),a),update:(a)=>e.set(t(a.id),a),remove:(a)=>e.remove(t(a)),list:()=>n(),depth:async()=>(await e.keys("queue:")).length}}import{createQueryKey as ee}from"@lssm/lib.contracts-spec/query/key";function x(e,t){if(e?.staleAt===void 0)return!1;return t>=e.staleAt}function A(){return{status:"idle",fetchStatus:"idle",cacheStatus:"empty",isStale:!1,isOffline:!1}}function U(e,t){if(e===void 0)return{status:"loading",fetchStatus:"fetching",cacheStatus:"empty",isStale:!1,isOffline:!1};let n=x(e,t);return{status:"success",fetchStatus:"fetching",data:e.data,cacheStatus:n?"stale":e.status,isStale:n,isOffline:!1,versionToken:e.versionToken,dataUpdatedAt:e.storedAt,...e.pageInfo===void 0?{}:{pageInfo:e.pageInfo}}}function Q(e,t){let n=x(e,t);return{status:"success",fetchStatus:"idle",data:e.data,cacheStatus:n?"stale":e.status,isStale:n,isOffline:!1,versionToken:e.versionToken,dataUpdatedAt:e.storedAt,...e.pageInfo===void 0?{}:{pageInfo:e.pageInfo}}}function F(e,t,n){return{status:"error",fetchStatus:"idle",data:t?.data,error:e,cacheStatus:t===void 0?"empty":t.status,isStale:x(t,n),isOffline:!1,versionToken:t?.versionToken,dataUpdatedAt:t?.storedAt,errorUpdatedAt:n}}function N(e,t,n){return{status:"auth-expired",fetchStatus:"idle",data:t?.data,error:e,cacheStatus:t===void 0?"empty":t.status,isStale:x(t,n),isOffline:!1,versionToken:t?.versionToken,dataUpdatedAt:t?.storedAt,errorUpdatedAt:n}}function V(e,t,n){return{status:e===void 0?"idle":"success",fetchStatus:"idle",data:e?.data,cacheStatus:"conflict",isStale:x(e,n),isOffline:!1,conflict:t,versionToken:t.versionToken??e?.versionToken,dataUpdatedAt:e?.storedAt}}function B(e,t){return{status:e===void 0?"idle":"success",fetchStatus:"idle",data:e?.data,cacheStatus:"offline",isStale:x(e,t),isOffline:!0,versionToken:e?.versionToken,dataUpdatedAt:e?.storedAt,...e?.pageInfo===void 0?{}:{pageInfo:e.pageInfo}}}function ce(e,t){let{transport:n}=e,a=e.advanced??{},i=a.now??(()=>Date.now()),u=a.reachability??q("online"),r=a.events??P(),d=a.resolvers??I(),M=t??K({defaultMode:e.localOffline,env:e.env,flag:e.flag}),T=new Map,h=new Map,E=0;function S(v,w){T.set(v,w);for(let m of h.get(v)??[])m(w)}return{...{async query(v,w){let m=ee(v),c=u.getSnapshot().signal;if(!C(c)){let l=B(void 0,i());return S(m,l),l}S(m,U(void 0,i()));let p=i(),s=await n.execute(v,w),y=i();if(!s.ok){r.emit({type:"fetch-error",key:m,code:s.code,latencyMs:y-p});let l=s.problem.category==="auth"?N(s.problem,void 0,y):F(s.problem,void 0,y);return S(m,l),l}r.emit({type:"fetch-success",key:m,latencyMs:y-p});let O={key:m,data:s.data.data,versionToken:s.data.versionToken??s.versionToken,status:c==="restricted"?"warm":"fresh",tags:[],storedAt:y,...s.data.pageInfo===void 0?{}:{pageInfo:s.data.pageInfo}},o=Q(O,y);return S(m,o),o},getState(v){return T.get(ee(v))??A()},subscribe(v,w){let m=ee(v),c=h.get(m);if(c===void 0)c=new Set,h.set(m,c);return c.add(w),()=>c?.delete(w)},async invalidateTags(){return[]},async refresh(v){let w=T.get(v)??A();return S(v,w),w},async emitConflict(v,w){let m=V(void 0,w,i());return S(v,m),m}},...{async mutate(v,w={}){E+=1;let m=`${i()}-${E}`;if(!C(u.getSnapshot().signal))return{id:m,status:"disabled-offline",problem:we()};let c=await n.execute({operation:v.operation,input:v.input,tags:v.tags},w.ctx??a.contextProvider?.());if(c.ok)return{id:m,status:"done",data:c.data.data};if(c.problem.category==="auth")return{id:m,status:"paused-auth",problem:c.problem};return{id:m,status:"failed",problem:c.problem}},async drain(){},async resume(){},dispose(){}},maintenance:{collectGarbage:async()=>0,revalidateStale:async()=>0},events:r,resolvers:d,localOffline:M}}function we(){return{type:"about:blank",title:"Offline mutation queue disabled",status:409,code:"OFFLINE_QUEUE_DISABLED",category:"conflict",severity:"warning",retryable:!1,detail:"localOffline=online-only does not cache local data or queue offline mutations."}}import{createQueryKey as ne}from"@lssm/lib.contracts-spec/query/key";function te(){let e=new Map;return{run(t,n){let a=e.get(t);if(a!==void 0)return a;let i=n().finally(()=>{e.delete(t)});return e.set(t,i),i},size(){return e.size}}}var Se=0;function ae(e){let{transport:t}=e,n=e.storage??X(),a=e.reachability??q("online"),i=e.now??(()=>Date.now()),u=e.defaultStaleTimeMs??Se,r=e.cacheStore??L(n),d=e.events??P(),M=te(),T=new Map,h=new Map;function E(c,p){T.set(c,p);let s=h.get(c);if(s===void 0)return;for(let y of s)y(p)}async function S(c,p){let s=ne(c),y=a.getSnapshot().signal,O=await r.read(s);if(!C(y)){let R=B(O,i());return E(s,R),R}if(O!==void 0&&!x(O,i())){d.emit({type:"cache-hit",key:s});let R=Q(O,i());return E(s,R),R}d.emit({type:"cache-miss",key:s}),E(s,U(O,i()));let o=i(),l=await M.run(s,()=>t.execute(c,p)),f=i();if(!l.ok){d.emit({type:"fetch-error",key:s,code:l.code,latencyMs:f-o});let R=l.problem.category==="auth"?N(l.problem,O,f):F(l.problem,O,f);return E(s,R),R}d.emit({type:"fetch-success",key:s,latencyMs:f-o});let g=c.cache?.staleTimeMs??u,k=c.cache?.cacheTimeMs,b={key:s,data:l.data.data,versionToken:l.data.versionToken??l.versionToken,status:y==="restricted"?"warm":"fresh",tags:c.tags??[],storedAt:f,staleAt:f+g,envelope:c,...k===void 0?{}:{expiresAt:f+k},...l.data.pageInfo===void 0?{}:{pageInfo:l.data.pageInfo}};await r.write(b);let ie=Q(b,f);return E(s,ie),ie}async function D(c){let p=await r.read(c),s=p===void 0?A():Q(p,i());return E(c,s),s}async function j(c,p){let s=await r.read(c),y=V(s,p,i());return E(c,y),y}function v(c){return T.get(ne(c))??A()}function w(c,p){let s=ne(c),y=h.get(s);if(y===void 0)y=new Set,h.set(s,y);return y.add(p),()=>{h.get(s)?.delete(p)}}async function m(c){let p=await r.invalidate(c,i());for(let s of p){let y=await r.read(s);if(y!==void 0)E(s,Q(y,i()))}return p}return{query:S,getState:v,subscribe:w,invalidateTags:m,refresh:D,emitConflict:j}}function oe(e){Te(e);let t=e.advanced??{},n=K({defaultMode:e.localOffline,env:e.env,flag:e.flag});if(be(n.mode,t),n.mode==="online-only")return ce(e,n);let a=t.now??(()=>Date.now()),i=t.storage??X(),u=t.events??P(),r=t.cacheStore??L(i),d=t.reachability??q("online"),M=t.resolvers??I(),T=Z(i),h=ae({...t,transport:e.transport,now:a,storage:i,events:u,cacheStore:r,reachability:d}),E=Y({transport:e.transport,cache:r,queue:T,reachability:d,resolvers:M,events:u,now:a,maxAttempts:t.maxAttempts,queryEngine:h,contextProvider:t.contextProvider,refreshAuth:t.refreshAuth,schedule:t.schedule,backoff:t.backoff}),S=H({cache:r,events:u,now:a,revalidate:(D)=>h.query(D)});return{...h,...E,maintenance:S,events:u,resolvers:M,localOffline:n}}function be(e,t){if(e==="offline-capable"){if(t.storage===void 0)throw Error("localOffline=offline-capable requires advanced.storage; pass a durable StoragePort or explicit platform storage adapter.");return}if(t.storage!==void 0)throw Error(`localOffline=${e} rejects advanced.storage; this mode must not use durable local persistence.`);if(t.cacheStore!==void 0)throw Error(`localOffline=${e} rejects advanced.cacheStore; this mode owns cache semantics.`)}function Te(e){let t=e;if(t.storage!==void 0)throw Error("Use advanced.storage instead of top-level storage.");if(t.cacheStore!==void 0)throw Error("Use advanced.cacheStore instead of top-level cacheStore.")}function ue(){return globalThis.process?.env}function ke(e){return oe({...e,env:e.env??ue()})}function De(){return{read:async(e)=>{return},write:async()=>{},remove:async()=>{},entries:async()=>[],invalidate:async(e,t)=>[]}}function Me(){return{type:"https://contractspec.io/problems/offline-queue-disabled",title:"Offline mutation queue disabled",status:503,code:"OFFLINE_QUEUE_DISABLED",category:"policy",severity:"warning",retryable:!0,detail:"localOffline is online-only; offline mutations are not queued or replayed."}}var Oe=(e)=>({operation:e.operation,input:e.input,tags:e.tags});function Ce(e){let t=e.now??(()=>Date.now()),n=0;async function a(i,u={}){n+=1;let r=`${t()}-${n}`;if(!C(e.reachability.getSnapshot().signal))return{id:r,status:"disabled-offline",problem:Me()};let d=await e.transport.execute(Oe(i),u.ctx);if(d.ok)return{id:r,status:"done",data:d.data.data};return{id:r,status:"failed",problem:d.problem}}return{mutate:a,drain:async()=>{},resume:async()=>{},dispose:()=>{}}}export{N as authExpiredState,W as backoffDelayMs,x as computeIsStale,V as conflictState,L as createCacheStore,I as createConflictResolverRegistry,oe as createDataEngine,ke as createDataEngineFromEnv,P as createEngineEvents,te as createInflightRegistry,H as createMaintenance,Y as createMutationEngine,Z as createMutationQueue,De as createNoCacheStore,Ce as createOnlineOnlyMutationEngine,ae as createQueryEngine,F as errorState,A as idleState,U as loadingState,B as offlineState,ue as readHostEnv,z as resolveQueuedConflict,_ as restoreSnapshots,Q as successState};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { QueryEnvelope } from '@lssm/lib.contracts-spec/query/envelope';
|
|
2
|
+
import type { CacheStore } from './cache-store.js';
|
|
3
|
+
import type { EngineEvents } from './events.js';
|
|
4
|
+
export interface MaintenanceDeps {
|
|
5
|
+
cache: CacheStore;
|
|
6
|
+
events: EngineEvents;
|
|
7
|
+
now: () => number;
|
|
8
|
+
/** Refetch a stale entry's source query (e.g. queryEngine.query). */
|
|
9
|
+
revalidate?: (envelope: QueryEnvelope) => Promise<unknown>;
|
|
10
|
+
}
|
|
11
|
+
/** Cache hygiene: garbage collection of expired entries + background revalidation. */
|
|
12
|
+
export interface Maintenance {
|
|
13
|
+
/** Evict entries whose `expiresAt` has passed; returns the evicted count. */
|
|
14
|
+
collectGarbage(): Promise<number>;
|
|
15
|
+
/** Refetch every stale entry that retained its source envelope; returns the count. */
|
|
16
|
+
revalidateStale(): Promise<number>;
|
|
17
|
+
}
|
|
18
|
+
export declare function createMaintenance(deps: MaintenanceDeps): Maintenance;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
function o(e){return{async collectGarbage(){let r=e.now(),n=0;for(let t of await e.cache.entries())if(t.expiresAt!==void 0&&r>=t.expiresAt)await e.cache.remove(t.key),n+=1;return e.events.emit({type:"gc-collected",evicted:n}),n},async revalidateStale(){let r=e.revalidate;if(r===void 0)return 0;let n=e.now(),t=0;for(let a of await e.cache.entries()){if(!(a.staleAt!==void 0&&n>=a.staleAt)||a.envelope===void 0)continue;await r(a.envelope),e.events.emit({type:"revalidated",key:a.key}),t+=1}return t}}}export{o as createMaintenance};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{mergeSafeEconomicEvidenceRefIds as Q,parseSafeEconomicEvidenceRefId as F}from"@lssm/lib.contracts-spec/operations";import{defaultConflictPolicy as I}from"@lssm/lib.contracts-spec/query/conflict-policy";function g(t){return t!=="offline"}function U(t="online"){let n={signal:t,since:Date.now()};return{getSnapshot:()=>n,subscribe:()=>()=>{}}}var B={id:"docs.contracts-runtime-core.reachability",title:"Reachability input port (tri-state)",summary:"Engine-owned reachability input port; integrations feed it, the engine never imports them.",kind:"reference",visibility:"internal",route:"/docs/contracts-runtime-core/reachability",domain:"contracts-runtime-core",owners:["@contractspec-core"],tags:["contracts-runtime-core","reachability","offline","port"],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/* \u2014 concrete sources (runtime-local networkReachability) are wired via an integration\u2192port adapter (plan \xA72.3/\xA74.2, Architect finding 1). isNetworkAllowed mirrors runtime-local's outboundNetworkAllowed = reachability !== 'offline'. createStaticReachabilitySource('online') is the B1a default and online test fixture."};function M(t,n={}){let i=n.baseMs??200,a=n.factor??2,u=n.maxMs??30000,m=n.jitter??Math.random,s=Math.max(0,t-1),l=Math.min(u,i*a**s);return Math.round(l*(0.5+m()*0.5))}import{defaultConflictPolicy as _}from"@lssm/lib.contracts-spec/query/conflict-policy";async function w(t,n){for(let i of t)if(i.previous===void 0)await n.remove(i.key);else await n.write(i.previous)}async function R(t,n,i){let a=t.conflictPolicy??_(),u=t.rollback[0]?.key,m=u===void 0?void 0:await i.cache.read(u),s=n.metadata?.serverData,l=await i.resolvers.resolve({strategy:a.strategy,policy:a,localData:m?.data,serverData:s,localVersion:t.versionToken,serverVersion:n.versionToken,mergeFields:a.mergeFields});switch(i.events.emit({type:"mutation-conflict",id:t.id,strategy:a.strategy,outcome:l.outcome}),l.outcome){case"use-server":{await w(t.rollback,i.cache),await i.cache.invalidate(t.tags,i.now());for(let E of t.rollback)await i.queryEngine?.refresh(E.key);return"resolved"}case"use-client":return await i.queue.update({...t,versionToken:n.versionToken,status:"pending"}),"re-enqueued";case"merged":{if(u!==void 0&&m!==void 0)await i.cache.write({...m,data:l.data,versionToken:n.versionToken,status:"warm",storedAt:i.now()}),await i.queryEngine?.refresh(u);return await i.queue.update({...t,versionToken:n.versionToken,status:"pending"}),"re-enqueued"}default:{if(u!==void 0)await i.queryEngine?.emitConflict(u,{versionToken:n.versionToken,serverData:s,clientData:m?.data,strategy:a.strategy});return"resolved"}}}var L=5;function b(...t){return Q(...t)}function N(t){let n=F(t);if(!n)throw Error("EconomicEvidenceInvalidRef: unsafe or invalid economic evidence ref");return n}function Y(t){let{transport:n,cache:i,queue:a,reachability:u,resolvers:m,events:s}=t,l=t.now??(()=>Date.now()),E=t.maxAttempts??L,S={cache:i,queue:a,resolvers:m,events:s,now:l,queryEngine:t.queryEngine},h=!1,k=0,y=null,p=new Map,T=(e,c=e.economicEvidenceRefs)=>({operation:e.operation,input:e.input,tags:e.tags,economicEvidenceRefs:c});async function A(e){let c=[];for(let r of e){let o=await i.read(r.key);c.push({key:r.key,previous:o}),await i.write({key:r.key,data:r.data,versionToken:r.versionToken??o?.versionToken,status:"warm",tags:o?.tags??[],storedAt:l(),staleAt:o?.staleAt,expiresAt:o?.expiresAt}),await t.queryEngine?.refresh(r.key)}return c}async function D(e,c){for(let r of e.rollback){let o=await i.read(r.key);if(o!==void 0)await i.write({...o,versionToken:c.versionToken??o.versionToken,status:"stale",staleAt:l()});await t.queryEngine?.refresh(r.key)}await i.invalidate(e.tags,l())}async function q(e){let c;try{c=b(e.economicEvidenceRefs)}catch{await w(e.rollback,i);for(let d of e.rollback)await t.queryEngine?.refresh(d.key);return await a.remove(e.id),s.emit({type:"mutation-failed",id:e.id,code:"ECONOMIC_EVIDENCE_INVALID_REF"}),p.set(e.id,{id:e.id,status:"failed",problem:{type:"about:blank",title:"Economic evidence ref rejected",status:422,code:"ECONOMIC_EVIDENCE_INVALID_REF",category:"validation",severity:"error",retryable:!1}}),!0}let r=e.attempts+1;await a.update({...e,status:"in-flight",attempts:r});let o=await n.execute(T(e,c),t.contextProvider?.());if(o.ok){let d=b(c,o.data.economicEvidenceRefs);if(await D(e,o.data),await a.remove(e.id),s.emit({type:"mutation-replayed",id:e.id,queueDepth:await a.depth()}),d.length>0)s.emit({type:"economic-evidence-attached",operationKey:e.operation.key,evidenceRefs:d.map(N)});return p.set(e.id,{id:e.id,status:"done",data:o.data.data,economicEvidenceRefs:d}),!0}let{problem:f}=o;if(f.category==="auth"){if(h=!0,await a.update({...e,status:"paused-auth",attempts:r}),s.emit({type:"auth-expired",id:e.id}),p.set(e.id,{id:e.id,status:"paused-auth",problem:f}),t.refreshAuth!==void 0&&await t.refreshAuth())return h=!1,await a.update({...e,status:"pending",attempts:r}),!0;return!1}if(f.category==="conflict"){if(await R(e,f,S)==="resolved")await a.remove(e.id),p.set(e.id,{id:e.id,status:"done",problem:f});return!0}if(f.retryable&&r<E){let d=M(r,t.backoff);return await a.update({...e,status:"failed",attempts:r}),s.emit({type:"mutation-retry",id:e.id,attempt:r,delayMs:d}),p.set(e.id,{id:e.id,status:"failed",problem:f}),t.schedule?.(()=>{v()},d),!0}await w(e.rollback,i);for(let d of e.rollback)await t.queryEngine?.refresh(d.key);return await a.remove(e.id),s.emit({type:"mutation-failed",id:e.id,code:o.code}),p.set(e.id,{id:e.id,status:"failed",problem:f}),!0}async function x(){if(h)return;if(!g(u.getSnapshot().signal))return;for(let e of await a.list()){if(e.status==="paused-auth")continue;if(!await q(e))return}s.emit({type:"queue-drained",remaining:await a.depth()})}function v(){if(y!==null)return y;return y=x().finally(()=>{y=null}),y}async function C(e,c={}){b(e.economicEvidenceRefs);let r=l();k+=1;let o=`${r}-${k}`,f=c.optimistic?await A(c.optimistic):[];if(await a.enqueue({...e,conflictPolicy:e.conflictPolicy??I(),id:o,enqueuedAt:r,attempts:0,status:"pending",rollback:f}),s.emit({type:"mutation-enqueued",id:o,queueDepth:await a.depth()}),!h&&g(u.getSnapshot().signal))await v();return p.get(o)??{id:o,status:"pending"}}async function O(){h=!1;for(let e of await a.list())if(e.status==="paused-auth")await a.update({...e,status:"pending"});await v()}let P=u.subscribe((e)=>{if(!h&&g(e.signal))v()});return{mutate:C,drain:v,resume:O,dispose:P}}export{Y as createMutationEngine};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { StoragePort } from '../storage/port.js';
|
|
2
|
+
import type { MutationQueueEntry } from './mutation-types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Durable, FIFO mutation queue layered on the {@link StoragePort}. Entries
|
|
5
|
+
* survive restart (with a durable storage impl) and are replayed in
|
|
6
|
+
* enqueue order on reconnect.
|
|
7
|
+
*/
|
|
8
|
+
export interface MutationQueue {
|
|
9
|
+
enqueue(entry: MutationQueueEntry): Promise<void>;
|
|
10
|
+
update(entry: MutationQueueEntry): Promise<void>;
|
|
11
|
+
remove(id: string): Promise<void>;
|
|
12
|
+
/** Entries ordered by `enqueuedAt` then `id` (stable FIFO). */
|
|
13
|
+
list(): Promise<readonly MutationQueueEntry[]>;
|
|
14
|
+
depth(): Promise<number>;
|
|
15
|
+
}
|
|
16
|
+
export declare function createMutationQueue(storage: StoragePort): MutationQueue;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
function s(t){let o=(e)=>`queue:${e}`;async function r(){let e=await t.keys("queue:"),i=[];for(let n of e){let u=await t.get(n);if(u!==void 0)i.push(u)}return i.sort((n,u)=>n.enqueuedAt===u.enqueuedAt?n.id.localeCompare(u.id):n.enqueuedAt-u.enqueuedAt),i}return{enqueue:(e)=>t.set(o(e.id),e),update:(e)=>t.set(o(e.id),e),remove:(e)=>t.remove(o(e)),list:()=>r(),depth:async()=>(await t.keys("queue:")).length}}export{s as createMutationQueue};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ConflictPolicy, ConflictStrategy } from '@lssm/lib.contracts-spec/query/conflict-policy';
|
|
2
|
+
import type { InvalidationTag } from '@lssm/lib.contracts-spec/query/invalidation';
|
|
3
|
+
import type { VersionToken } from '@lssm/lib.contracts-spec/query/version-token';
|
|
4
|
+
import type { ContractProblem } from '@lssm/lib.contracts-spec/results';
|
|
5
|
+
import type { ReachabilitySource } from '../reachability.js';
|
|
6
|
+
import type { Transport, TransportContext } from '../transport.js';
|
|
7
|
+
import type { BackoffOptions } from './backoff.js';
|
|
8
|
+
import type { CacheStore } from './cache-store.js';
|
|
9
|
+
import type { EngineEvents } from './events.js';
|
|
10
|
+
import type { MutationQueue } from './mutation-queue.js';
|
|
11
|
+
import type { CacheEntry, QueryEngine } from './types.js';
|
|
12
|
+
/** Lifecycle of a queued mutation. `paused-auth` blocks the drain until refresh. */
|
|
13
|
+
export type MutationQueueStatus = 'pending' | 'in-flight' | 'failed' | 'paused-auth';
|
|
14
|
+
/** What a caller submits to mutate; persisted (minus runtime fields) in the queue. */
|
|
15
|
+
export interface MutationRequest<TInput = unknown> {
|
|
16
|
+
operation: {
|
|
17
|
+
key: string;
|
|
18
|
+
version?: string;
|
|
19
|
+
};
|
|
20
|
+
input: TInput;
|
|
21
|
+
tags: readonly InvalidationTag[];
|
|
22
|
+
/** Version token the client last observed for the target entity (etag). */
|
|
23
|
+
versionToken?: VersionToken;
|
|
24
|
+
conflictPolicy?: ConflictPolicy;
|
|
25
|
+
/** Safe economic evidence refs carried into transport and replay. */
|
|
26
|
+
economicEvidenceRefs?: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
/** A cache snapshot captured before optimistic apply, restored on rollback. */
|
|
29
|
+
export interface OptimisticSnapshot {
|
|
30
|
+
key: string;
|
|
31
|
+
/** Prior entry; `undefined` means the key was absent and is removed on rollback. */
|
|
32
|
+
previous?: CacheEntry;
|
|
33
|
+
}
|
|
34
|
+
/** A caller-supplied optimistic cache write keyed by canonical query key. */
|
|
35
|
+
export interface OptimisticWrite<TData = unknown> {
|
|
36
|
+
key: string;
|
|
37
|
+
data: TData;
|
|
38
|
+
versionToken?: VersionToken;
|
|
39
|
+
}
|
|
40
|
+
export interface MutateOptions {
|
|
41
|
+
ctx?: TransportContext;
|
|
42
|
+
optimistic?: readonly OptimisticWrite[];
|
|
43
|
+
}
|
|
44
|
+
export interface MutationQueueEntry<TInput = unknown> extends MutationRequest<TInput> {
|
|
45
|
+
id: string;
|
|
46
|
+
enqueuedAt: number;
|
|
47
|
+
attempts: number;
|
|
48
|
+
status: MutationQueueStatus;
|
|
49
|
+
rollback: readonly OptimisticSnapshot[];
|
|
50
|
+
}
|
|
51
|
+
export interface MutationResult<TData = unknown> {
|
|
52
|
+
id: string;
|
|
53
|
+
status: MutationQueueStatus | 'done' | 'disabled-offline';
|
|
54
|
+
data?: TData;
|
|
55
|
+
problem?: ContractProblem<string>;
|
|
56
|
+
conflictOutcome?: ConflictOutcomeKind;
|
|
57
|
+
/** Safe economic evidence refs observed from request/result. */
|
|
58
|
+
economicEvidenceRefs?: readonly string[];
|
|
59
|
+
}
|
|
60
|
+
export type ConflictOutcomeKind = 'use-server' | 'use-client' | 'merged' | 'surface';
|
|
61
|
+
/** Context handed to a conflict resolver. */
|
|
62
|
+
export interface ConflictContext<TData = unknown> {
|
|
63
|
+
strategy: ConflictStrategy;
|
|
64
|
+
policy: ConflictPolicy;
|
|
65
|
+
localData: TData | undefined;
|
|
66
|
+
serverData: TData | undefined;
|
|
67
|
+
localVersion?: VersionToken;
|
|
68
|
+
serverVersion?: VersionToken;
|
|
69
|
+
mergeFields?: readonly string[];
|
|
70
|
+
}
|
|
71
|
+
export type ConflictResolution<TData = unknown> = {
|
|
72
|
+
outcome: 'use-server';
|
|
73
|
+
data: TData | undefined;
|
|
74
|
+
} | {
|
|
75
|
+
outcome: 'use-client';
|
|
76
|
+
data: TData | undefined;
|
|
77
|
+
} | {
|
|
78
|
+
outcome: 'merged';
|
|
79
|
+
data: TData;
|
|
80
|
+
} | {
|
|
81
|
+
outcome: 'surface';
|
|
82
|
+
};
|
|
83
|
+
export type ConflictResolver<TData = unknown> = (ctx: ConflictContext<TData>) => ConflictResolution<TData> | Promise<ConflictResolution<TData>>;
|
|
84
|
+
export interface ConflictResolverRegistry {
|
|
85
|
+
/** Register a `crdt`/`custom` resolver under a policy `resolver` key. */
|
|
86
|
+
register(key: string, resolver: ConflictResolver): void;
|
|
87
|
+
has(key: string): boolean;
|
|
88
|
+
resolve(ctx: ConflictContext): Promise<ConflictResolution>;
|
|
89
|
+
}
|
|
90
|
+
export interface MutationEngineOptions {
|
|
91
|
+
transport: Transport;
|
|
92
|
+
cache: CacheStore;
|
|
93
|
+
queue: MutationQueue;
|
|
94
|
+
reachability: ReachabilitySource;
|
|
95
|
+
resolvers: ConflictResolverRegistry;
|
|
96
|
+
events: EngineEvents;
|
|
97
|
+
now?: () => number;
|
|
98
|
+
/** Max replay attempts before a queued mutation is rolled back + dropped. */
|
|
99
|
+
maxAttempts?: number;
|
|
100
|
+
/** Query engine notified after optimistic apply / rollback / conflict. */
|
|
101
|
+
queryEngine?: Pick<QueryEngine, 'refresh' | 'emitConflict'>;
|
|
102
|
+
/** Supplies fresh auth/correlation context for each replay (AC14, no-strip). */
|
|
103
|
+
contextProvider?: () => TransportContext | undefined;
|
|
104
|
+
/** Refresh expired auth on replay; resolves true when a fresh token is ready. */
|
|
105
|
+
refreshAuth?: () => Promise<boolean>;
|
|
106
|
+
/** Schedule a delayed retry drain; when omitted, retryable entries wait for the next drain. */
|
|
107
|
+
schedule?: (run: () => void, delayMs: number) => void;
|
|
108
|
+
backoff?: BackoffOptions;
|
|
109
|
+
}
|
|
110
|
+
export interface MutationEngine {
|
|
111
|
+
mutate<TData = unknown>(request: MutationRequest, options?: MutateOptions): Promise<MutationResult<TData>>;
|
|
112
|
+
/** Replay queued mutations when online; safe to call repeatedly. */
|
|
113
|
+
drain(): Promise<void>;
|
|
114
|
+
/** Resume a queue paused by auth expiry (after a successful refresh). */
|
|
115
|
+
resume(): Promise<void>;
|
|
116
|
+
/** Stop the reachability subscription (test/teardown hygiene). */
|
|
117
|
+
dispose(): void;
|
|
118
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type LocalOfflinePolicy } from '../local-offline.js';
|
|
2
|
+
import type { DataEngine, DataEngineOptions } from './data-engine.js';
|
|
3
|
+
export declare function createOnlineOnlyDataEngine(options: DataEngineOptions,
|
|
4
|
+
/**
|
|
5
|
+
* Pre-resolved policy from the caller (e.g. `createDataEngine`) so the
|
|
6
|
+
* trigger is resolved exactly once. When omitted (direct construction) the
|
|
7
|
+
* policy is resolved here from the same env/flag/default inputs.
|
|
8
|
+
*/
|
|
9
|
+
resolvedPolicy?: LocalOfflinePolicy): DataEngine;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{createQueryKey as T}from"@lssm/lib.contracts-spec/query/key";var F=new Set(["online-only","memory-cache","offline-capable"]),O={"online-only":"online-only",online:"online-only",disabled:"online-only",false:"online-only",off:"online-only","memory-cache":"memory-cache",memory:"memory-cache",cache:"memory-cache","offline-capable":"offline-capable",offline:"offline-capable",enabled:"offline-capable",true:"offline-capable",on:"offline-capable"};function G(e){return typeof e==="string"&&F.has(e)}function D(e={}){let t=e.defaultMode===void 0?{mode:"online-only",source:"default",reason:"No local/offline policy input was supplied."}:b(e.defaultMode,"explicit")??{mode:"online-only",source:"explicit"},n=b(_(e.env),"env",{allowUnknown:!0});return b(e.flag,"feature-flag")??n??t}function _(e){if(typeof e!=="object"||e===null)return e;if("CONTRACTSPEC_LOCAL_OFFLINE"in e)return e.CONTRACTSPEC_LOCAL_OFFLINE;if("mode"in e)return e;return}function b(e,t,n={}){if(e===void 0||e===null||e==="")return;if(typeof e==="boolean")return{mode:e?"offline-capable":"online-only",source:t};if(typeof e==="string"){let o=O[e.trim().toLowerCase()];if(o!==void 0)return{mode:o,source:t};if(n.allowUnknown)return}if(typeof e==="object"&&e!==null&&"mode"in e){let o=e,l=typeof o.mode==="string"?O[o.mode.trim().toLowerCase()]:void 0;if(l!==void 0)return{mode:l,source:t,...typeof o.reason==="string"?{reason:o.reason}:{}};if(n.allowUnknown)return}throw Error(`Unsupported localOffline policy value: ${String(e)}`)}function S(e){return e!=="offline"}function C(e="online"){let t={signal:e,since:Date.now()};return{getSnapshot:()=>t,subscribe:()=>()=>{}}}var H={id:"docs.contracts-runtime-core.reachability",title:"Reachability input port (tri-state)",summary:"Engine-owned reachability input port; integrations feed it, the engine never imports them.",kind:"reference",visibility:"internal",route:"/docs/contracts-runtime-core/reachability",domain:"contracts-runtime-core",owners:["@contractspec-core"],tags:["contracts-runtime-core","reachability","offline","port"],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/* \u2014 concrete sources (runtime-local networkReachability) are wired via an integration\u2192port adapter (plan \xA72.3/\xA74.2, Architect finding 1). isNetworkAllowed mirrors runtime-local's outboundNetworkAllowed = reachability !== 'offline'. createStaticReachabilitySource('online') is the B1a default and online test fixture."};function j(e){let{serverData:t,localData:n}=e,o=e.mergeFields??[];if(typeof t!=="object"||t===null||typeof n!=="object"||n===null)return{outcome:"surface"};let l={...t},y=n;for(let u of o)if(u in y)l[u]=y[u];return{outcome:"merged",data:l}}function R(){let e=new Map;return{register(t,n){e.set(t,n)},has(t){return e.has(t)},async resolve(t){switch(t.strategy){case"server-wins":return{outcome:"use-server",data:t.serverData};case"client-wins":return{outcome:"use-client",data:t.localData};case"field-merge":return j(t);case"surface":return{outcome:"surface"};default:{let n=t.policy.resolver,o=n===void 0?void 0:e.get(n);if(o===void 0)return{outcome:"surface"};return o(t)}}}}}function L(){let e=new Set;return{emit(t){for(let n of e)n(t)},subscribe(t){return e.add(t),()=>{e.delete(t)}}}}function p(e,t){if(e?.staleAt===void 0)return!1;return t>=e.staleAt}function v(){return{status:"idle",fetchStatus:"idle",cacheStatus:"empty",isStale:!1,isOffline:!1}}function x(e,t){if(e===void 0)return{status:"loading",fetchStatus:"fetching",cacheStatus:"empty",isStale:!1,isOffline:!1};let n=p(e,t);return{status:"success",fetchStatus:"fetching",data:e.data,cacheStatus:n?"stale":e.status,isStale:n,isOffline:!1,versionToken:e.versionToken,dataUpdatedAt:e.storedAt,...e.pageInfo===void 0?{}:{pageInfo:e.pageInfo}}}function A(e,t){let n=p(e,t);return{status:"success",fetchStatus:"idle",data:e.data,cacheStatus:n?"stale":e.status,isStale:n,isOffline:!1,versionToken:e.versionToken,dataUpdatedAt:e.storedAt,...e.pageInfo===void 0?{}:{pageInfo:e.pageInfo}}}function M(e,t,n){return{status:"error",fetchStatus:"idle",data:t?.data,error:e,cacheStatus:t===void 0?"empty":t.status,isStale:p(t,n),isOffline:!1,versionToken:t?.versionToken,dataUpdatedAt:t?.storedAt,errorUpdatedAt:n}}function Q(e,t,n){return{status:"auth-expired",fetchStatus:"idle",data:t?.data,error:e,cacheStatus:t===void 0?"empty":t.status,isStale:p(t,n),isOffline:!1,versionToken:t?.versionToken,dataUpdatedAt:t?.storedAt,errorUpdatedAt:n}}function P(e,t,n){return{status:e===void 0?"idle":"success",fetchStatus:"idle",data:e?.data,cacheStatus:"conflict",isStale:p(e,n),isOffline:!1,conflict:t,versionToken:t.versionToken??e?.versionToken,dataUpdatedAt:e?.storedAt}}function I(e,t){return{status:e===void 0?"idle":"success",fetchStatus:"idle",data:e?.data,cacheStatus:"offline",isStale:p(e,t),isOffline:!0,versionToken:e?.versionToken,dataUpdatedAt:e?.storedAt,...e?.pageInfo===void 0?{}:{pageInfo:e.pageInfo}}}function ie(e,t){let{transport:n}=e,o=e.advanced??{},l=o.now??(()=>Date.now()),y=o.reachability??C("online"),u=o.events??L(),q=o.resolvers??R(),U=t??D({defaultMode:e.localOffline,env:e.env,flag:e.flag}),g=new Map,h=new Map,w=0;function f(i,r){g.set(i,r);for(let a of h.get(i)??[])a(r)}return{...{async query(i,r){let a=T(i),s=y.getSnapshot().signal;if(!S(s)){let m=I(void 0,l());return f(a,m),m}f(a,x(void 0,l()));let k=l(),c=await n.execute(i,r),d=l();if(!c.ok){u.emit({type:"fetch-error",key:a,code:c.code,latencyMs:d-k});let m=c.problem.category==="auth"?Q(c.problem,void 0,d):M(c.problem,void 0,d);return f(a,m),m}u.emit({type:"fetch-success",key:a,latencyMs:d-k});let N={key:a,data:c.data.data,versionToken:c.data.versionToken??c.versionToken,status:s==="restricted"?"warm":"fresh",tags:[],storedAt:d,...c.data.pageInfo===void 0?{}:{pageInfo:c.data.pageInfo}},E=A(N,d);return f(a,E),E},getState(i){return g.get(T(i))??v()},subscribe(i,r){let a=T(i),s=h.get(a);if(s===void 0)s=new Set,h.set(a,s);return s.add(r),()=>s?.delete(r)},async invalidateTags(){return[]},async refresh(i){let r=g.get(i)??v();return f(i,r),r},async emitConflict(i,r){let a=P(void 0,r,l());return f(i,a),a}},...{async mutate(i,r={}){w+=1;let a=`${l()}-${w}`;if(!S(y.getSnapshot().signal))return{id:a,status:"disabled-offline",problem:B()};let s=await n.execute({operation:i.operation,input:i.input,tags:i.tags},r.ctx??o.contextProvider?.());if(s.ok)return{id:a,status:"done",data:s.data.data};if(s.problem.category==="auth")return{id:a,status:"paused-auth",problem:s.problem};return{id:a,status:"failed",problem:s.problem}},async drain(){},async resume(){},dispose(){}},maintenance:{collectGarbage:async()=>0,revalidateStale:async()=>0},events:u,resolvers:q,localOffline:U}}function B(){return{type:"about:blank",title:"Offline mutation queue disabled",status:409,code:"OFFLINE_QUEUE_DISABLED",category:"conflict",severity:"warning",retryable:!1,detail:"localOffline=online-only does not cache local data or queue offline mutations."}}export{ie as createOnlineOnlyDataEngine};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { MutationEngine, MutationEngineOptions } from './mutation-types.js';
|
|
2
|
+
/** Online-only mutation strategy: direct online writes, no local queue/replay. */
|
|
3
|
+
export declare function createOnlineOnlyMutationEngine(options: Pick<MutationEngineOptions, 'transport' | 'reachability' | 'now'>): MutationEngine;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
function a(e){return e!=="offline"}function p(e="online"){let t={signal:e,since:Date.now()};return{getSnapshot:()=>t,subscribe:()=>()=>{}}}var d={id:"docs.contracts-runtime-core.reachability",title:"Reachability input port (tri-state)",summary:"Engine-owned reachability input port; integrations feed it, the engine never imports them.",kind:"reference",visibility:"internal",route:"/docs/contracts-runtime-core/reachability",domain:"contracts-runtime-core",owners:["@contractspec-core"],tags:["contracts-runtime-core","reachability","offline","port"],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/* \u2014 concrete sources (runtime-local networkReachability) are wired via an integration\u2192port adapter (plan \xA72.3/\xA74.2, Architect finding 1). isNetworkAllowed mirrors runtime-local's outboundNetworkAllowed = reachability !== 'offline'. createStaticReachabilitySource('online') is the B1a default and online test fixture."};function c(){return{type:"https://contractspec.io/problems/offline-queue-disabled",title:"Offline mutation queue disabled",status:503,code:"OFFLINE_QUEUE_DISABLED",category:"policy",severity:"warning",retryable:!0,detail:"localOffline is online-only; offline mutations are not queued or replayed."}}var u=(e)=>({operation:e.operation,input:e.input,tags:e.tags});function y(e){let t=e.now??(()=>Date.now()),o=0;async function r(s,l={}){o+=1;let n=`${t()}-${o}`;if(!a(e.reachability.getSnapshot().signal))return{id:n,status:"disabled-offline",problem:c()};let i=await e.transport.execute(u(s),l.ctx);if(i.ok)return{id:n,status:"done",data:i.data.data};return{id:n,status:"failed",problem:i.problem}}return{mutate:r,drain:async()=>{},resume:async()=>{},dispose:()=>{}}}export{y as createOnlineOnlyMutationEngine};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { QueryEngine, QueryEngineOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates the B1a synchronous query engine. Wires an in-house cache store,
|
|
4
|
+
* request dedup, the canonical {@link QueryState} machine, tag-invalidation, an
|
|
5
|
+
* in-memory storage default, and reachability consumed purely via the port.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createQueryEngine(options: QueryEngineOptions): QueryEngine;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{createQueryKey as w}from"@lssm/lib.contracts-spec/query/key";function Q(e){return e!=="offline"}function A(e="online"){let t={signal:e,since:Date.now()};return{getSnapshot:()=>t,subscribe:()=>()=>{}}}var G={id:"docs.contracts-runtime-core.reachability",title:"Reachability input port (tri-state)",summary:"Engine-owned reachability input port; integrations feed it, the engine never imports them.",kind:"reference",visibility:"internal",route:"/docs/contracts-runtime-core/reachability",domain:"contracts-runtime-core",owners:["@contractspec-core"],tags:["contracts-runtime-core","reachability","offline","port"],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/* \u2014 concrete sources (runtime-local networkReachability) are wired via an integration\u2192port adapter (plan \xA72.3/\xA74.2, Architect finding 1). isNetworkAllowed mirrors runtime-local's outboundNetworkAllowed = reachability !== 'offline'. createStaticReachabilitySource('online') is the B1a default and online test fixture."};function P(){let e=new Map;return{get:async(t)=>{let n=e.get(t);return n===void 0?void 0:structuredClone(n)},set:async(t,n)=>{e.set(t,structuredClone(n))},remove:async(t)=>{e.delete(t)},keys:async(t)=>{let n=[...e.keys()];return t===void 0?n:n.filter((r)=>r.startsWith(t))},clear:async()=>{e.clear()}}}import{invalidationTagMatches as W}from"@lssm/lib.contracts-spec/query/invalidation";var I="cache:";function R(e){let t=new Map,n=(r)=>`${I}${r}`;return{read:(r)=>e.get(n(r)),write:async(r)=>{t.set(r.key,r.tags),await e.set(n(r.key),r)},remove:async(r)=>{t.delete(r),await e.remove(n(r))},entries:async()=>{let r=await e.keys(I),s=[];for(let g of r){let u=await e.get(g);if(u!==void 0)s.push(u)}return s},invalidate:async(r,s)=>{let g=[];for(let[u,h]of t.entries()){if(!h.some((T)=>r.some((l)=>W(T,l))))continue;let S=await e.get(n(u));if(S===void 0)continue;await e.set(n(u),{...S,status:"stale",staleAt:s}),g.push(u)}return g}}}function M(){let e=new Map;return{run(t,n){let r=e.get(t);if(r!==void 0)return r;let s=n().finally(()=>{e.delete(t)});return e.set(t,s),s},size(){return e.size}}}function O(){let e=new Set;return{emit(t){for(let n of e)n(t)},subscribe(t){return e.add(t),()=>{e.delete(t)}}}}function m(e,t){if(e?.staleAt===void 0)return!1;return t>=e.staleAt}function v(){return{status:"idle",fetchStatus:"idle",cacheStatus:"empty",isStale:!1,isOffline:!1}}function q(e,t){if(e===void 0)return{status:"loading",fetchStatus:"fetching",cacheStatus:"empty",isStale:!1,isOffline:!1};let n=m(e,t);return{status:"success",fetchStatus:"fetching",data:e.data,cacheStatus:n?"stale":e.status,isStale:n,isOffline:!1,versionToken:e.versionToken,dataUpdatedAt:e.storedAt,...e.pageInfo===void 0?{}:{pageInfo:e.pageInfo}}}function b(e,t){let n=m(e,t);return{status:"success",fetchStatus:"idle",data:e.data,cacheStatus:n?"stale":e.status,isStale:n,isOffline:!1,versionToken:e.versionToken,dataUpdatedAt:e.storedAt,...e.pageInfo===void 0?{}:{pageInfo:e.pageInfo}}}function L(e,t,n){return{status:"error",fetchStatus:"idle",data:t?.data,error:e,cacheStatus:t===void 0?"empty":t.status,isStale:m(t,n),isOffline:!1,versionToken:t?.versionToken,dataUpdatedAt:t?.storedAt,errorUpdatedAt:n}}function V(e,t,n){return{status:"auth-expired",fetchStatus:"idle",data:t?.data,error:e,cacheStatus:t===void 0?"empty":t.status,isStale:m(t,n),isOffline:!1,versionToken:t?.versionToken,dataUpdatedAt:t?.storedAt,errorUpdatedAt:n}}function U(e,t,n){return{status:e===void 0?"idle":"success",fetchStatus:"idle",data:e?.data,cacheStatus:"conflict",isStale:m(e,n),isOffline:!1,conflict:t,versionToken:t.versionToken??e?.versionToken,dataUpdatedAt:e?.storedAt}}function N(e,t){return{status:e===void 0?"idle":"success",fetchStatus:"idle",data:e?.data,cacheStatus:"offline",isStale:m(e,t),isOffline:!0,versionToken:e?.versionToken,dataUpdatedAt:e?.storedAt,...e?.pageInfo===void 0?{}:{pageInfo:e.pageInfo}}}var X=0;function de(e){let{transport:t}=e,n=e.storage??P(),r=e.reachability??A("online"),s=e.now??(()=>Date.now()),g=e.defaultStaleTimeMs??X,u=e.cacheStore??R(n),h=e.events??O(),D=M(),S=new Map,T=new Map;function l(i,o){S.set(i,o);let a=T.get(i);if(a===void 0)return;for(let c of a)c(o)}async function K(i,o){let a=w(i),c=r.getSnapshot().signal,y=await u.read(a);if(!Q(c)){let p=N(y,s());return l(a,p),p}if(y!==void 0&&!m(y,s())){h.emit({type:"cache-hit",key:a});let p=b(y,s());return l(a,p),p}h.emit({type:"cache-miss",key:a}),l(a,q(y,s()));let k=s(),d=await D.run(a,()=>t.execute(i,o)),f=s();if(!d.ok){h.emit({type:"fetch-error",key:a,code:d.code,latencyMs:f-k});let p=d.problem.category==="auth"?V(d.problem,y,f):L(d.problem,y,f);return l(a,p),p}h.emit({type:"fetch-success",key:a,latencyMs:f-k});let H=i.cache?.staleTimeMs??g,E=i.cache?.cacheTimeMs,C={key:a,data:d.data.data,versionToken:d.data.versionToken??d.versionToken,status:c==="restricted"?"warm":"fresh",tags:i.tags??[],storedAt:f,staleAt:f+H,envelope:i,...E===void 0?{}:{expiresAt:f+E},...d.data.pageInfo===void 0?{}:{pageInfo:d.data.pageInfo}};await u.write(C);let x=b(C,f);return l(a,x),x}async function _(i){let o=await u.read(i),a=o===void 0?v():b(o,s());return l(i,a),a}async function z(i,o){let a=await u.read(i),c=U(a,o,s());return l(i,c),c}function B(i){return S.get(w(i))??v()}function F(i,o){let a=w(i),c=T.get(a);if(c===void 0)c=new Set,T.set(a,c);return c.add(o),()=>{T.get(a)?.delete(o)}}async function j(i){let o=await u.invalidate(i,s());for(let a of o){let c=await u.read(a);if(c!==void 0)l(a,b(c,s()))}return o}return{query:K,getState:B,subscribe:F,invalidateTags:j,refresh:_,emitConflict:z}}export{de as createQueryEngine};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { QueryConflict, QueryState } from '@lssm/lib.contracts-spec/query/query-state';
|
|
2
|
+
import type { ContractProblem } from '@lssm/lib.contracts-spec/results';
|
|
3
|
+
import type { CacheEntry } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Pure {@link QueryState} builders — the canonical state machine for B1a. Each
|
|
6
|
+
* returns a render-ready snapshot for I/O-free design-system components; no
|
|
7
|
+
* fetching, storage, or side effects happen here.
|
|
8
|
+
*
|
|
9
|
+
* The three axes are orthogonal: `status` (data lifecycle), `fetchStatus`
|
|
10
|
+
* (in-flight network), and `cacheStatus`/`isOffline`/`conflict` (freshness,
|
|
11
|
+
* source, divergence). Offline and conflict are NOT statuses — they are flags.
|
|
12
|
+
*/
|
|
13
|
+
/** True when the entry has a stale deadline that `now` has reached. */
|
|
14
|
+
export declare function computeIsStale(entry: CacheEntry | undefined, now: number): boolean;
|
|
15
|
+
export declare function idleState<TData = unknown>(): QueryState<TData>;
|
|
16
|
+
/** Fetch about to start: `loading` when no data, success-while-fetching otherwise. */
|
|
17
|
+
export declare function loadingState<TData = unknown>(entry: CacheEntry<TData> | undefined, now: number): QueryState<TData>;
|
|
18
|
+
export declare function successState<TData = unknown>(entry: CacheEntry<TData>, now: number): QueryState<TData>;
|
|
19
|
+
export declare function errorState<TData = unknown>(problem: ContractProblem, entry: CacheEntry<TData> | undefined, now: number): QueryState<TData>;
|
|
20
|
+
/**
|
|
21
|
+
* Auth-expiry snapshot: a read that hit an `auth`-category problem. Recoverable
|
|
22
|
+
* (status `auth-expired`, not a generic `error`) so the host can drive a
|
|
23
|
+
* refresh-and-retry; prior cached data is preserved for display.
|
|
24
|
+
*/
|
|
25
|
+
export declare function authExpiredState<TData = unknown>(problem: ContractProblem, entry: CacheEntry<TData> | undefined, now: number): QueryState<TData>;
|
|
26
|
+
/**
|
|
27
|
+
* Conflict snapshot: conflict is a flag, not a status. Keep the (optimistic)
|
|
28
|
+
* data as `success` with `cacheStatus: 'conflict'` + the `conflict` detail so
|
|
29
|
+
* design-system can render a resolve/merge prompt.
|
|
30
|
+
*/
|
|
31
|
+
export declare function conflictState<TData = unknown>(entry: CacheEntry<TData> | undefined, conflict: QueryConflict<TData>, now: number): QueryState<TData>;
|
|
32
|
+
/**
|
|
33
|
+
* Offline snapshot: offline is a flag, not a status. Serve cached data as
|
|
34
|
+
* `success` when present; otherwise stay `idle` (no fetch is attempted). Always
|
|
35
|
+
* `isOffline: true` + `cacheStatus: 'offline'`, `fetchStatus: 'idle'`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function offlineState<TData = unknown>(entry: CacheEntry<TData> | undefined, now: number): QueryState<TData>;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
function s(t,e){if(t?.staleAt===void 0)return!1;return e>=t.staleAt}function n(){return{status:"idle",fetchStatus:"idle",cacheStatus:"empty",isStale:!1,isOffline:!1}}function u(t,e){if(t===void 0)return{status:"loading",fetchStatus:"fetching",cacheStatus:"empty",isStale:!1,isOffline:!1};let a=s(t,e);return{status:"success",fetchStatus:"fetching",data:t.data,cacheStatus:a?"stale":t.status,isStale:a,isOffline:!1,versionToken:t.versionToken,dataUpdatedAt:t.storedAt,...t.pageInfo===void 0?{}:{pageInfo:t.pageInfo}}}function o(t,e){let a=s(t,e);return{status:"success",fetchStatus:"idle",data:t.data,cacheStatus:a?"stale":t.status,isStale:a,isOffline:!1,versionToken:t.versionToken,dataUpdatedAt:t.storedAt,...t.pageInfo===void 0?{}:{pageInfo:t.pageInfo}}}function i(t,e,a){return{status:"error",fetchStatus:"idle",data:e?.data,error:t,cacheStatus:e===void 0?"empty":e.status,isStale:s(e,a),isOffline:!1,versionToken:e?.versionToken,dataUpdatedAt:e?.storedAt,errorUpdatedAt:a}}function d(t,e,a){return{status:"auth-expired",fetchStatus:"idle",data:e?.data,error:t,cacheStatus:e===void 0?"empty":e.status,isStale:s(e,a),isOffline:!1,versionToken:e?.versionToken,dataUpdatedAt:e?.storedAt,errorUpdatedAt:a}}function f(t,e,a){return{status:t===void 0?"idle":"success",fetchStatus:"idle",data:t?.data,cacheStatus:"conflict",isStale:s(t,a),isOffline:!1,conflict:e,versionToken:e.versionToken??t?.versionToken,dataUpdatedAt:t?.storedAt}}function r(t,e){return{status:t===void 0?"idle":"success",fetchStatus:"idle",data:t?.data,cacheStatus:"offline",isStale:s(t,e),isOffline:!0,versionToken:t?.versionToken,dataUpdatedAt:t?.storedAt,...t?.pageInfo===void 0?{}:{pageInfo:t.pageInfo}}}export{d as authExpiredState,s as computeIsStale,f as conflictState,i as errorState,n as idleState,u as loadingState,r as offlineState,o as successState};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { CacheStatus } from '@lssm/lib.contracts-spec/query/cache-status';
|
|
2
|
+
import type { QueryEnvelope, QueryPageInfo } from '@lssm/lib.contracts-spec/query/envelope';
|
|
3
|
+
import type { InvalidationTag } from '@lssm/lib.contracts-spec/query/invalidation';
|
|
4
|
+
import type { QueryConflict, QueryState } from '@lssm/lib.contracts-spec/query/query-state';
|
|
5
|
+
import type { VersionToken } from '@lssm/lib.contracts-spec/query/version-token';
|
|
6
|
+
import type { ReachabilitySource } from '../reachability.js';
|
|
7
|
+
import type { StoragePort } from '../storage/port.js';
|
|
8
|
+
import type { Transport, TransportContext } from '../transport.js';
|
|
9
|
+
import type { CacheStore } from './cache-store.js';
|
|
10
|
+
import type { EngineEvents } from './events.js';
|
|
11
|
+
/**
|
|
12
|
+
* A persisted cache record, layered on top of the value-agnostic
|
|
13
|
+
* {@link StoragePort} and keyed by the canonical `createQueryKey`.
|
|
14
|
+
*/
|
|
15
|
+
export interface CacheEntry<TData = unknown> {
|
|
16
|
+
key: string;
|
|
17
|
+
data: TData;
|
|
18
|
+
versionToken?: VersionToken;
|
|
19
|
+
status: CacheStatus;
|
|
20
|
+
/** Tags this read depends on; drives tag invalidation. */
|
|
21
|
+
tags: readonly InvalidationTag[];
|
|
22
|
+
/** Epoch ms the value was stored. */
|
|
23
|
+
storedAt: number;
|
|
24
|
+
/** Epoch ms the value becomes stale; omitted = never stale. */
|
|
25
|
+
staleAt?: number;
|
|
26
|
+
/** Epoch ms the value is eligible for garbage collection; omitted = never. */
|
|
27
|
+
expiresAt?: number;
|
|
28
|
+
/** Source envelope, retained so background revalidation can refetch by key. */
|
|
29
|
+
envelope?: QueryEnvelope;
|
|
30
|
+
/** Pagination flags (cursor + hasNextPage) from the result envelope, if any. */
|
|
31
|
+
pageInfo?: QueryPageInfo;
|
|
32
|
+
}
|
|
33
|
+
export type QueryStateListener<TData = unknown> = (state: QueryState<TData>) => void;
|
|
34
|
+
export interface QueryEngineOptions {
|
|
35
|
+
transport: Transport;
|
|
36
|
+
/** Durable cache/queue store; defaults to in-memory (B1a). */
|
|
37
|
+
storage?: StoragePort;
|
|
38
|
+
/** Reachability input; defaults to a static `online` source (B1a). */
|
|
39
|
+
reachability?: ReachabilitySource;
|
|
40
|
+
/** Clock injection for deterministic tests. */
|
|
41
|
+
now?: () => number;
|
|
42
|
+
/** Fallback stale window (ms) when an envelope omits `cache.staleTimeMs`. */
|
|
43
|
+
defaultStaleTimeMs?: number;
|
|
44
|
+
/** Shared cache store (so query + mutation engines coordinate); defaults to a fresh one. */
|
|
45
|
+
cacheStore?: CacheStore;
|
|
46
|
+
/** Shared engine event sink; defaults to a fresh one. */
|
|
47
|
+
events?: EngineEvents;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The framework-neutral query engine. B1a drives the synchronous online path
|
|
51
|
+
* plus the `isOffline` flag; `conflict` and `auth-expired` transitions land in
|
|
52
|
+
* B1b.
|
|
53
|
+
*/
|
|
54
|
+
export interface QueryEngine {
|
|
55
|
+
query<TData = unknown>(envelope: QueryEnvelope, ctx?: TransportContext): Promise<QueryState<TData>>;
|
|
56
|
+
getState<TData = unknown>(envelope: QueryEnvelope): QueryState<TData>;
|
|
57
|
+
subscribe<TData = unknown>(envelope: QueryEnvelope, listener: QueryStateListener<TData>): () => void;
|
|
58
|
+
invalidateTags(tags: readonly InvalidationTag[]): Promise<readonly string[]>;
|
|
59
|
+
/**
|
|
60
|
+
* Re-derive and emit the state for a cache key from its current stored entry
|
|
61
|
+
* (used by the mutation engine after optimistic apply / rollback). Emits a
|
|
62
|
+
* success state when an entry exists, otherwise an idle state.
|
|
63
|
+
*/
|
|
64
|
+
refresh<TData = unknown>(key: string): Promise<QueryState<TData>>;
|
|
65
|
+
/**
|
|
66
|
+
* Surface a conflict on a cache key: emits a `cacheStatus: 'conflict'` state
|
|
67
|
+
* carrying the `conflict` detail for design-system resolve/merge prompts.
|
|
68
|
+
*/
|
|
69
|
+
emitConflict<TData = unknown>(key: string, conflict: QueryConflict<TData>): Promise<QueryState<TData>>;
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FormDocument } from '@lssm/lib.contracts-spec/forms';
|
|
2
|
+
export interface FormCsvColumn {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly labelKey: string;
|
|
5
|
+
readonly sensitive: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function formCsvColumns(document: FormDocument): readonly FormCsvColumn[];
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{flattenPortableFormFields as p}from"@lssm/lib.contracts-spec/forms";function u(e){let r=new Set(e.questions.map(({name:o})=>o)),l=e.questions.map(c),s=new Set(e.formSpec?.policy?.pii??[]),m=p(e.formSpec?.fields??[]),a=Object.entries(e.formSpec?.modelFields??{}).flatMap(([o,i])=>{if(r.has(o))return[];let t=m.find((n)=>n.name===o);return[{id:o,labelKey:t?.labelI18n??o,sensitive:s.has(o)||[...s].some((n)=>n.startsWith(`${o}.`))||t?.kind==="email"||t?.kind==="phone"||i.type==="email"||i.type==="phone"}]});return[...l,...a]}function c(e){return{id:e.id,labelKey:e.labelKey,sensitive:e.sensitive===!0||e.kind==="email"||e.kind==="phone"}}export{u as formCsvColumns};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FormResponse, FormRevision } from '@lssm/lib.contracts-spec/forms';
|
|
2
|
+
export interface FormCsvExportOptions {
|
|
3
|
+
locale?: string;
|
|
4
|
+
includeSensitive?: boolean;
|
|
5
|
+
sensitiveGrant?: SensitiveFormExportGrant;
|
|
6
|
+
}
|
|
7
|
+
export interface SensitiveFormExportGrant {
|
|
8
|
+
authorized: true;
|
|
9
|
+
auditReceiptId: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function serializeFormResponsesCsv(revision: FormRevision, responses: readonly FormResponse[], options?: FormCsvExportOptions): string;
|
|
12
|
+
export declare function streamFormResponsesCsv(revision: FormRevision, responses: AsyncIterable<FormResponse>, options?: FormCsvExportOptions): AsyncGenerator<string>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{flattenPortableFormFields as v}from"@lssm/lib.contracts-spec/forms";function u(e){let o=new Set(e.questions.map(({name:t})=>t)),n=e.questions.map(y),r=new Set(e.formSpec?.policy?.pii??[]),i=v(e.formSpec?.fields??[]),m=Object.entries(e.formSpec?.modelFields??{}).flatMap(([t,c])=>{if(o.has(t))return[];let s=i.find((p)=>p.name===t);return[{id:t,labelKey:s?.labelI18n??t,sensitive:r.has(t)||[...r].some((p)=>p.startsWith(`${t}.`))||s?.kind==="email"||s?.kind==="phone"||c.type==="email"||c.type==="phone"}]});return[...n,...m]}function y(e){return{id:e.id,labelKey:e.labelKey,sensitive:e.sensitive===!0||e.kind==="email"||e.kind==="phone"}}import{compileFormDocument as g,validateFormDocument as R}from"@lssm/lib.contracts-spec/forms";class a extends Error{code;constructor(e,o){super(o);this.code=e;this.name="FormsPlatformError"}}function O(e,o){let n=e?.owner.product===o.product&&e.owner.tenantRef===o.tenantRef&&e.owner.workspaceRef===o.workspaceRef&&e.owner.ownerRef===o.ownerRef;if(!e||!n)throw new a("FORM_NOT_FOUND","Form definition was not found in the caller scope");return e}function P(e){let o=R(e);if(o.length)throw new a("INVALID_FORM_DOCUMENT",o.map((n)=>n.message).join("; "));g(e)}function b(e,o){if(o==="none")return!0;if(o==="either")return e!=="anonymous";if(o==="both")return e==="both";return e===o||e==="both"}function x(e,o){if(b(o,e.verification.requirement))return"completed";return e.verification.unverifiedDisposition==="pending-hidden"?"pending-verification":"completed-unverified"}function f(e,o){let n=new Set(e.content.enabledLocales.map((r)=>r.toLowerCase()));for(let r of o){if(!r)continue;let i=r.trim().toLowerCase(),m=e.content.enabledLocales.find((s)=>s.toLowerCase()===i);if(n.has(i)&&m)return m;let t=i.split("-")[0],c=e.content.enabledLocales.find((s)=>s.toLowerCase().split("-")[0]===t);if(c)return c}return e.content.defaultLocale}var D="response_extensions:AI follow-ups";function w(e){let o=Array.isArray(e)?e.every((r)=>typeof r!=="object")?e.join(";"):JSON.stringify(e):e==null?"":typeof e==="object"?JSON.stringify(e):String(e);return`"${(/^[\s]*[=+\-@]/.test(o)?`'${o}`:o).replaceAll('"','""')}"`}function l(e){return`${e.map(w).join(",")}\r
|
|
3
|
+
`}function d(e,o,n){if(o.revisionId!==e.id)throw Error("CSV responses must belong to the exported revision");let r=u(e.document),i=new Set(r.map(({id:t})=>t)),m=Object.fromEntries(Object.entries(o.answers).filter(([t])=>!i.has(t)));return[o.id,o.status,o.locale,o.submittedAt??"",...r.map((t)=>{if(!(n.includeSensitive&&n.sensitiveGrant?.authorized)&&t.sensitive)return"[REDACTED]";return o.answers[t.id]}),n.includeSensitive&&n.sensitiveGrant?.authorized?m:"[REDACTED]"]}function F(e,o){let n=f(e.document,[o]),r=e.document.content.catalogs[n]??{};return["response_id","status","locale","submitted_at",...u(e.document).map((i)=>`${i.id}:${r[i.labelKey]??i.labelKey}`),D]}function _(e,o,n={}){return`\uFEFF${l(F(e,n.locale))}${o.map((r)=>l(d(e,r,n))).join("")}`}async function*N(e,o,n={}){yield`\uFEFF${l(F(e,n.locale))}`;for await(let r of o)yield l(d(e,r,n))}export{_ as serializeFormResponsesCsv,N as streamFormResponsesCsv};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FormDefinition, FormDocument, FormOwnerScope, FormRevision } from '@lssm/lib.contracts-spec/forms';
|
|
2
|
+
import { type FormsPlatformDependencies } from './forms-platform-types.js';
|
|
3
|
+
export declare function createFormsManagementService(dependencies: FormsPlatformDependencies): {
|
|
4
|
+
create(scope: FormOwnerScope, document: FormDocument): Promise<FormDefinition>;
|
|
5
|
+
saveDraft(scope: FormOwnerScope, definitionId: string, expectedVersion: number, document: FormDocument): Promise<FormDefinition>;
|
|
6
|
+
publish(scope: FormOwnerScope, definitionId: string, expectedVersion: number): Promise<{
|
|
7
|
+
definition: FormDefinition;
|
|
8
|
+
revision: FormRevision;
|
|
9
|
+
}>;
|
|
10
|
+
close(scope: FormOwnerScope, definitionId: string): Promise<FormDefinition>;
|
|
11
|
+
archive(scope: FormOwnerScope, definitionId: string): Promise<FormDefinition>;
|
|
12
|
+
};
|