@jarenjs/contract 0.75.0 → 0.83.3
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 +9 -0
- package/dist/types/app/index.d.ts +1 -1
- package/dist/types/app/subscription.d.ts +10 -0
- package/dist/types/command.d.ts +27 -0
- package/dist/types/errors.d.ts +4 -0
- package/dist/types/provider/compile.d.ts +73 -0
- package/dist/types/provider/execute.d.ts +104 -0
- package/dist/types/provider/index.d.ts +4 -0
- package/dist/types/provider/run.d.ts +33 -0
- package/docs/CONTRACT-FORMAT.md +41 -0
- package/docs/DURABLE.md +66 -0
- package/docs/PROVIDER-FORMAT.md +157 -0
- package/package.json +14 -6
- package/src/app/index.js +1 -1
- package/src/app/subscription.js +21 -0
- package/src/client/http.js +3 -40
- package/src/command.js +70 -0
- package/src/errors.js +4 -0
- package/src/provider/compile.js +200 -0
- package/src/provider/execute.js +191 -0
- package/src/provider/index.js +5 -0
- package/src/provider/run.js +138 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/** Run lifetimes over the existing identify/acquire/release coordinator. */
|
|
3
|
+
import { canonicalizeJson } from '@jarenjs/json/canonical';
|
|
4
|
+
import { deepFreeze } from '@jarenjs/core/object';
|
|
5
|
+
import { resolveLifecycle, identify, acquire, once } from '../host.js';
|
|
6
|
+
import { createProviderExecutor, providerHostError } from './execute.js';
|
|
7
|
+
|
|
8
|
+
/** Privileged host objects cannot belong to two concurrent runs.
|
|
9
|
+
* @type {WeakSet<object>} */
|
|
10
|
+
const leased = new WeakSet();
|
|
11
|
+
const FIELDS = ['runId', 'actor', 'environment', 'destination', 'revision', 'lease'];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {{ runId: string, actor: string, environment: string,
|
|
15
|
+
* destination: string, revision: string, lease: string }} ProviderAuthority
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Run a callback with private, isolated transport resources. current() must
|
|
20
|
+
* re-read live authority and return the matching evidence, or null to revoke.
|
|
21
|
+
* The host owns membership, destination resolution, OAuth and account locks.
|
|
22
|
+
* Only opaque evidence and JSON callback results can leave this lifetime.
|
|
23
|
+
* @param {ProviderAuthority} authority
|
|
24
|
+
* @param {{ identify: Function, acquire: Function,
|
|
25
|
+
* current: (evidence: ProviderAuthority, host: any, operation: any) => ProviderAuthority | null | Promise<ProviderAuthority | null>,
|
|
26
|
+
* transport: (request: import('./execute.js').ProviderRequest, context: any) => Promise<Response> }} host
|
|
27
|
+
* @param {(run: any) => any} work
|
|
28
|
+
* @param {import('./execute.js').ProviderExecutorOptions & { signal?: AbortSignal }} [options]
|
|
29
|
+
* @returns {Promise<any>}
|
|
30
|
+
*/
|
|
31
|
+
export async function withProviderRun(authority, host, work, options = {}) {
|
|
32
|
+
if (!authority || Object.keys(authority).length !== FIELDS.length
|
|
33
|
+
|| FIELDS.some((key) => typeof authority[key] !== 'string' || !authority[key]))
|
|
34
|
+
throw providerHostError('authority contains only runId, actor, environment, destination, revision and lease opaque strings');
|
|
35
|
+
if (!host || ['identify', 'acquire', 'current', 'transport'].some((name) => typeof host[name] !== 'function') || typeof work !== 'function')
|
|
36
|
+
throw providerHostError('run needs identify, acquire, current, transport and work capabilities');
|
|
37
|
+
const evidence = deepFreeze(JSON.parse(canonicalizeJson(authority)));
|
|
38
|
+
const lifecycle = resolveLifecycle(host, providerHostError);
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const signal = AbortSignal.any([controller.signal, ...(options.signal ? [options.signal] : [])]);
|
|
41
|
+
let live = true;
|
|
42
|
+
let fault = false;
|
|
43
|
+
const observe = () => { fault = true; };
|
|
44
|
+
const refused = (reason) => ({ state: 'refused', reason, evidence });
|
|
45
|
+
if (signal.aborted) return refused('cancelled');
|
|
46
|
+
const identity = await identify(lifecycle, /** @type {any} */ ({ carrier: 'provider', trace: evidence.runId, signal, evidence }));
|
|
47
|
+
if (identity.kind !== 'lease') return refused('host-fault');
|
|
48
|
+
const releaseIdentity = once(identity.lease.release, observe);
|
|
49
|
+
let releaseAcquired = async () => true;
|
|
50
|
+
let executor;
|
|
51
|
+
let enterPromise;
|
|
52
|
+
let privileged;
|
|
53
|
+
let owns = false;
|
|
54
|
+
/** @type {Set<Promise<any>>} */
|
|
55
|
+
const operations = new Set();
|
|
56
|
+
const track = (promise) => {
|
|
57
|
+
operations.add(promise);
|
|
58
|
+
promise.then(() => operations.delete(promise), () => operations.delete(promise));
|
|
59
|
+
return promise;
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
if (signal.aborted) return refused('cancelled');
|
|
63
|
+
const entered = await acquire(lifecycle, evidence, { host: identity.lease.host }, (lease) => {
|
|
64
|
+
releaseAcquired = once(lease.release, observe);
|
|
65
|
+
enterPromise = (async () => {
|
|
66
|
+
if (!live || signal.aborted) return refused('cancelled');
|
|
67
|
+
privileged = lease.host;
|
|
68
|
+
if (privileged === null || typeof privileged !== 'object') return refused('host-fault');
|
|
69
|
+
if (leased.has(privileged)) {
|
|
70
|
+
releaseAcquired = async () => true;
|
|
71
|
+
return refused('resource-in-use');
|
|
72
|
+
}
|
|
73
|
+
leased.add(privileged);
|
|
74
|
+
owns = true;
|
|
75
|
+
const check = async (operation) => {
|
|
76
|
+
if (!live || signal.aborted) return false;
|
|
77
|
+
let current;
|
|
78
|
+
try { current = await host.current(evidence, privileged, operation); }
|
|
79
|
+
catch { current = null; }
|
|
80
|
+
const allowed = current && FIELDS.every((key) => current[key] === evidence[key]);
|
|
81
|
+
if (!allowed) controller.abort();
|
|
82
|
+
return Boolean(allowed && live && !signal.aborted);
|
|
83
|
+
};
|
|
84
|
+
executor = createProviderExecutor({ ...options,
|
|
85
|
+
transport: (request, context) => host.transport(request, { ...context, host: privileged }),
|
|
86
|
+
});
|
|
87
|
+
const run = Object.freeze(Object.defineProperties({ evidence }, {
|
|
88
|
+
signal: { value: signal },
|
|
89
|
+
execute: { value: (request, context = {}) => track(executor.execute(request, {
|
|
90
|
+
...context, signal: AbortSignal.any([signal, ...(context.signal ? [context.signal] : [])]),
|
|
91
|
+
beforeDispatch: () => check({ phase: 'dispatch', url: request.url, method: request.method }),
|
|
92
|
+
})) },
|
|
93
|
+
check: { value: () => track(check({ phase: 'publish' })) },
|
|
94
|
+
publish: { value: (value, commit) => track((async () => {
|
|
95
|
+
if (!await check({ phase: 'publish' })) return refused('authority-changed');
|
|
96
|
+
return commit(JSON.parse(canonicalizeJson(value)), evidence);
|
|
97
|
+
})()) },
|
|
98
|
+
}));
|
|
99
|
+
if (!await check({ phase: 'start' })) return refused('authority-changed');
|
|
100
|
+
try {
|
|
101
|
+
const value = await work(run);
|
|
102
|
+
if (signal.aborted || !live) return refused('cancelled');
|
|
103
|
+
return { state: 'complete', evidence, value: JSON.parse(canonicalizeJson(value)) };
|
|
104
|
+
}
|
|
105
|
+
catch { return refused('run-failed'); }
|
|
106
|
+
finally {
|
|
107
|
+
live = false;
|
|
108
|
+
controller.abort();
|
|
109
|
+
await executor.close();
|
|
110
|
+
await Promise.allSettled([...operations]);
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
return enterPromise;
|
|
114
|
+
});
|
|
115
|
+
if (entered.kind !== 'entered' || entered.afterFault) {
|
|
116
|
+
live = false;
|
|
117
|
+
controller.abort();
|
|
118
|
+
await executor?.close();
|
|
119
|
+
await enterPromise;
|
|
120
|
+
return refused('host-fault');
|
|
121
|
+
}
|
|
122
|
+
return entered.result;
|
|
123
|
+
}
|
|
124
|
+
finally { await cleanup(); }
|
|
125
|
+
|
|
126
|
+
async function cleanup() {
|
|
127
|
+
live = false;
|
|
128
|
+
controller.abort();
|
|
129
|
+
await executor?.close();
|
|
130
|
+
await Promise.allSettled([...operations]);
|
|
131
|
+
// The host may restore a switched account in release. Every old request,
|
|
132
|
+
// callback and publication has settled before either release begins.
|
|
133
|
+
await releaseAcquired();
|
|
134
|
+
if (owns) leased.delete(privileged);
|
|
135
|
+
await releaseIdentity();
|
|
136
|
+
if (fault) throw providerHostError('provider resource release failed');
|
|
137
|
+
}
|
|
138
|
+
}
|