@toa.io/extensions.continuity 1.0.0-alpha.321
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/components/continuity.journal/manifest.toa.yaml +47 -0
- package/components/continuity.journal/migrations/0002-runs.yaml +11 -0
- package/components/continuity.journal/operations/recall.js +3 -0
- package/components/continuity.journal/operations/record.js +9 -0
- package/components/continuity.journal/types/index.d.ts +3 -0
- package/components/continuity.journal/types/toa.d.ts +41 -0
- package/package.json +35 -0
- package/readme.md +150 -0
- package/transpiled/Composition.d.ts +14 -0
- package/transpiled/Composition.js +30 -0
- package/transpiled/Composition.js.map +1 -0
- package/transpiled/Factory.d.ts +21 -0
- package/transpiled/Factory.js +156 -0
- package/transpiled/Factory.js.map +1 -0
- package/transpiled/Journal.d.ts +24 -0
- package/transpiled/Journal.js +57 -0
- package/transpiled/Journal.js.map +1 -0
- package/transpiled/Run.d.ts +52 -0
- package/transpiled/Run.js +109 -0
- package/transpiled/Run.js.map +1 -0
- package/transpiled/answers.d.ts +29 -0
- package/transpiled/answers.js +61 -0
- package/transpiled/answers.js.map +1 -0
- package/transpiled/fingerprint.d.ts +8 -0
- package/transpiled/fingerprint.js +60 -0
- package/transpiled/fingerprint.js.map +1 -0
- package/transpiled/index.d.ts +3 -0
- package/transpiled/index.js +3 -0
- package/transpiled/index.js.map +1 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
namespace: continuity
|
|
2
|
+
name: journal
|
|
3
|
+
|
|
4
|
+
# One record per step of a run, under an id derived from the run and the step, so that a step is
|
|
5
|
+
# written once whoever writes it first.
|
|
6
|
+
entity:
|
|
7
|
+
associated: true
|
|
8
|
+
properties:
|
|
9
|
+
run:
|
|
10
|
+
description: The identity of the task or the event the step was made for
|
|
11
|
+
type: string
|
|
12
|
+
answer:
|
|
13
|
+
description: What the step answered, encoded by the extension
|
|
14
|
+
type: object
|
|
15
|
+
expires:
|
|
16
|
+
description: >
|
|
17
|
+
When the run can no longer be picked up. Every step of a run carries the moment its
|
|
18
|
+
first step fixed, so a run is reaped whole.
|
|
19
|
+
type: integer
|
|
20
|
+
format: epoch-millis
|
|
21
|
+
|
|
22
|
+
operations:
|
|
23
|
+
# keeps an answer unless one is kept already, and answers what is kept
|
|
24
|
+
record:
|
|
25
|
+
type: transition
|
|
26
|
+
# two copies of a run writing one step race to create it, and the loser reads the winner's
|
|
27
|
+
concurrency: retry
|
|
28
|
+
input:
|
|
29
|
+
type: object
|
|
30
|
+
properties:
|
|
31
|
+
run:
|
|
32
|
+
type: string
|
|
33
|
+
answer:
|
|
34
|
+
type: object
|
|
35
|
+
expires:
|
|
36
|
+
type: integer
|
|
37
|
+
required:
|
|
38
|
+
- run
|
|
39
|
+
- answer
|
|
40
|
+
- expires
|
|
41
|
+
output:
|
|
42
|
+
type: object
|
|
43
|
+
# what a run has kept
|
|
44
|
+
recall:
|
|
45
|
+
type: observation
|
|
46
|
+
scope: entries
|
|
47
|
+
query: true
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Written by `toa types`. Every run rewrites it.
|
|
2
|
+
// What a manifest does not state belongs in a file of your own.
|
|
3
|
+
|
|
4
|
+
import type { Query } from '@toa.io/core/types'
|
|
5
|
+
import type { Readable } from 'node:stream'
|
|
6
|
+
|
|
7
|
+
export interface Entity {
|
|
8
|
+
/** The identity of the task or the event the step was made for */
|
|
9
|
+
run?: string
|
|
10
|
+
/** What the step answered, encoded by the extension */
|
|
11
|
+
answer?: Record<string, unknown>
|
|
12
|
+
/** When the run can no longer be picked up. Every step of a run carries the moment its first step fixed, so a run is reaped whole. */
|
|
13
|
+
expires?: number
|
|
14
|
+
id: string
|
|
15
|
+
VERSION: number
|
|
16
|
+
CREATED: number
|
|
17
|
+
UPDATED: number
|
|
18
|
+
DELETED: number | null
|
|
19
|
+
REGION: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type State = Entity & { DISCARD: boolean, TRAILERS: Record<string, unknown> }
|
|
23
|
+
|
|
24
|
+
export type RecordInput = {
|
|
25
|
+
run: string
|
|
26
|
+
answer: Record<string, unknown>
|
|
27
|
+
expires: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type RecordOutput = Record<string, unknown>
|
|
31
|
+
|
|
32
|
+
export interface Component {
|
|
33
|
+
record: (request: { input: RecordInput, query?: Query<Entity>, task?: boolean }) => Promise<RecordOutput>
|
|
34
|
+
recall: (request: { input?: null, query: Query<Entity>, task?: boolean }) => Promise<unknown>
|
|
35
|
+
assign: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<Entity>
|
|
36
|
+
ensure: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<Entity>
|
|
37
|
+
enumerate: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<Entity[]>
|
|
38
|
+
observe: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<Entity | null>
|
|
39
|
+
stream: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<Readable>
|
|
40
|
+
terminate: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<Entity>
|
|
41
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@toa.io/extensions.continuity",
|
|
3
|
+
"version": "1.0.0-alpha.321",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Toa Continuity",
|
|
6
|
+
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
7
|
+
"homepage": "https://github.com/toa-io/toa#readme",
|
|
8
|
+
"main": "transpiled/index.js",
|
|
9
|
+
"types": "transpiled/index.d.ts",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/toa-io/toa.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/toa-io/toa/issues"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"transpiled",
|
|
22
|
+
"components",
|
|
23
|
+
"readme.md",
|
|
24
|
+
"!**/tsconfig.tsbuildinfo",
|
|
25
|
+
"!**/*.test.*",
|
|
26
|
+
"!**/.env*"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@toa.io/core": "1.0.0-alpha.321",
|
|
30
|
+
"@toa.io/definitions": "1.0.0-alpha.321"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"transpile": "tsc"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Toa Continuity
|
|
2
|
+
|
|
3
|
+
An operation that picks up where it failed.
|
|
4
|
+
|
|
5
|
+
A [task](/documentation/tasks.md), or an event a receiver hands on, is tried again when it raises,
|
|
6
|
+
and every attempt runs the operation from the top. An operation this extension continues is given
|
|
7
|
+
back, on a later attempt, whatever its context answered on an earlier one: the calls, fetches and
|
|
8
|
+
stashes that already answered are not made again, and what is made is the step that failed and
|
|
9
|
+
what follows it.
|
|
10
|
+
|
|
11
|
+
## Declaring it
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
# manifest.toa.yaml
|
|
15
|
+
operations:
|
|
16
|
+
onboard:
|
|
17
|
+
type: effect
|
|
18
|
+
|
|
19
|
+
continuity:
|
|
20
|
+
onboard: 604800 # seconds an unfinished run can still be resumed
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Each operation named is an `effect` of the component, with how long, in seconds, a run of it that
|
|
24
|
+
has not finished can still be picked up. There is no default: how late a parked task can be put
|
|
25
|
+
back is yours to know. The window is at least `600`, and a manifest that states less, names an
|
|
26
|
+
operation the component does not have, or names one that is not an effect is refused.
|
|
27
|
+
|
|
28
|
+
The operation is written as any other:
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
async function effect(input, context) {
|
|
32
|
+
const account = await context.remote.identity.accounts.create({ input })
|
|
33
|
+
await context.fetch('crm', { method: 'POST', body: JSON.stringify(account) })
|
|
34
|
+
await context.remote.mail.messages.send({ input: { to: account.email } })
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If `send` raises, the next attempt is given the account `create` answered and the response the
|
|
39
|
+
CRM gave, and only `send` is made again.
|
|
40
|
+
|
|
41
|
+
## Calling it
|
|
42
|
+
|
|
43
|
+
As a task, or from a receiver — where nobody waits for the answer:
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
await context.call('users.onboarding.onboard', { input, task: true })
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Any other call is refused, and so is a pulse naming it.
|
|
50
|
+
|
|
51
|
+
## What you can count on
|
|
52
|
+
|
|
53
|
+
**What answered is not asked again.** Within one run — a task, or one event — a step that answered
|
|
54
|
+
is given back on every later attempt: after an exception, after the process died, after a redeploy,
|
|
55
|
+
and after a parked task is put back within the window. A task delivered again after it finished is
|
|
56
|
+
given everything back, and makes nothing.
|
|
57
|
+
|
|
58
|
+
**Concurrent branches are fine.** A step is recognized by what it asks, not by when:
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
await Promise.all([a().then(b), c().then(b)])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Each `b` is given its own answer however `a` and `c` complete on the next attempt.
|
|
65
|
+
|
|
66
|
+
**Two copies of a run agree.** Where a run is delivered twice at once, both copies go on with the
|
|
67
|
+
answer the first of them kept. A value taken at once — `context.id()`, `context.now()`,
|
|
68
|
+
`context.random()` — is the exception: each copy has its own until it is kept, so a step one copy
|
|
69
|
+
builds from it may be made by the other as well.
|
|
70
|
+
|
|
71
|
+
**A call keeps its identity.** A call made from the operation carries the same identity on every
|
|
72
|
+
attempt, so a callee that declares [`once`](/documentation/inbox.md) refuses a repeat of it.
|
|
73
|
+
|
|
74
|
+
## What that asks of you
|
|
75
|
+
|
|
76
|
+
**The next attempt may run somewhere else.** A task or an event that is tried again is taken by
|
|
77
|
+
whichever replica is free, and after a restart or a redeploy by a process that has never seen the run.
|
|
78
|
+
What the context answered is given back wherever that is; what the operation kept in its own process
|
|
79
|
+
is not. `context.state`, a variable of the module, a cache in memory, a file on the local disk — on the
|
|
80
|
+
next attempt each of them is whatever that process holds, which is not what the earlier attempt left:
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
// counts in whichever process the attempt runs in, and not across attempts
|
|
84
|
+
context.state.sent = (context.state.sent ?? 0) + 1
|
|
85
|
+
|
|
86
|
+
// kept with the run, and given back wherever the next attempt runs
|
|
87
|
+
const sent = await context.remote.mail.messages.send({ input })
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
So nothing a later attempt depends on is carried in the process: take it from a step, or from what
|
|
91
|
+
the operation writes and reads back.
|
|
92
|
+
|
|
93
|
+
**Take time, randomness and new ids from the context.** A step is recognized by its arguments, so an
|
|
94
|
+
argument that differs between attempts makes a new step, and the step is made again:
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
// a new timestamp every attempt: the call is made again every attempt
|
|
98
|
+
await context.remote.billing.invoices.issue({ input: { at: Date.now() } })
|
|
99
|
+
|
|
100
|
+
// the same timestamp on every attempt: the call is made once
|
|
101
|
+
await context.remote.billing.invoices.issue({ input: { at: context.now() } })
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`context.now()`, `context.random()` and `context.id()` are given back like any other step. The same
|
|
105
|
+
holds for the entity an effect reads: it is read again on every attempt, and a step whose arguments
|
|
106
|
+
come from it is made again if it moved.
|
|
107
|
+
|
|
108
|
+
**A step that took effect may still be made again.** Where the process dies after a step took
|
|
109
|
+
effect and before its answer was kept, the next attempt makes it again. A call into Toa arrives with
|
|
110
|
+
the same identity, so a callee that declares `once` refuses it; a `fetch` to a third party is sent
|
|
111
|
+
again, so make it safe to repeat, or pass it a key it deduplicates on.
|
|
112
|
+
|
|
113
|
+
**Among identical steps, which branch gets which answer is not fixed.** Two concurrent `b(x)` are
|
|
114
|
+
the same request, and each is given one of the two answers — possibly the other one than the first
|
|
115
|
+
time. A later step built from the answer together with something of the branch's own has new
|
|
116
|
+
arguments, and is made again. `context.id()` twice in two branches is the common case.
|
|
117
|
+
|
|
118
|
+
**What cannot be kept is refused.** A step that answers with a stream raises `Unrecordable`, and
|
|
119
|
+
the run is parked. A `fetch` is kept whole — its status, its headers and its body — and given back
|
|
120
|
+
as a `Response`. An answer is kept as one MongoDB document, so one past its 16 MB fails to be kept,
|
|
121
|
+
and the step raises.
|
|
122
|
+
|
|
123
|
+
**An answer is given as it is kept, the first time too.** What a step answers is given back as the
|
|
124
|
+
broker carries it, on the attempt that made it as on any later one, so the operation reads the same
|
|
125
|
+
thing every time.
|
|
126
|
+
|
|
127
|
+
**A run that used up its attempts is parked**, as any task is. It picks up where it stopped if it is
|
|
128
|
+
put back within its window; after that, it starts from the beginning.
|
|
129
|
+
|
|
130
|
+
## Not steps
|
|
131
|
+
|
|
132
|
+
`logs`, `span`, `metrics`, `configuration`, `state` and `atom` are what they are on every attempt,
|
|
133
|
+
and nothing of them is kept. `state` in particular belongs to one process, and the next attempt may
|
|
134
|
+
run in another.
|
|
135
|
+
|
|
136
|
+
## Deployment
|
|
137
|
+
|
|
138
|
+
The runs are kept in the extension's own component, `continuity.journal`, in MongoDB. A context
|
|
139
|
+
whose components declare `continuity` deploys it as a service of its own. Its database is given as
|
|
140
|
+
for any component, so a context whose `mongodb` names one address for everything has nothing to add;
|
|
141
|
+
one that names them per namespace names this one too:
|
|
142
|
+
|
|
143
|
+
```yaml
|
|
144
|
+
# context.toa.yaml
|
|
145
|
+
mongodb:
|
|
146
|
+
todos: mongodb://todos.mongo.example.com
|
|
147
|
+
continuity: mongodb://mongo.example.com
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
See [deployment](/documentation/deployment.md).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Connector } from '@toa.io/core';
|
|
2
|
+
import type { Host } from './Factory.ts';
|
|
3
|
+
/** The components this extension ships, which run inside its own service. */
|
|
4
|
+
export declare function components(): Components;
|
|
5
|
+
export declare class Composition extends Connector {
|
|
6
|
+
private readonly host;
|
|
7
|
+
constructor(host: Host);
|
|
8
|
+
protected open(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
interface Components {
|
|
11
|
+
labels: string[];
|
|
12
|
+
paths: string[];
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { Connector } from '@toa.io/core';
|
|
4
|
+
/** The components this extension ships, which run inside its own service. */
|
|
5
|
+
export function components() {
|
|
6
|
+
const labels = [];
|
|
7
|
+
const paths = [];
|
|
8
|
+
for (const entry of entries()) {
|
|
9
|
+
labels.push(entry.name.replace('.', '-'));
|
|
10
|
+
paths.push(resolve(ROOT, entry.name));
|
|
11
|
+
}
|
|
12
|
+
return { labels, paths };
|
|
13
|
+
}
|
|
14
|
+
export class Composition extends Connector {
|
|
15
|
+
host;
|
|
16
|
+
constructor(host) {
|
|
17
|
+
super();
|
|
18
|
+
this.host = host;
|
|
19
|
+
}
|
|
20
|
+
async open() {
|
|
21
|
+
const composition = await this.host.composition(components().paths);
|
|
22
|
+
await composition.connect();
|
|
23
|
+
this.depends(composition);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function entries() {
|
|
27
|
+
return readdirSync(ROOT, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
28
|
+
}
|
|
29
|
+
const ROOT = resolve(import.meta.dirname, '../components/');
|
|
30
|
+
//# sourceMappingURL=Composition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Composition.js","sourceRoot":"","sources":["../source/Composition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAIxC,6EAA6E;AAC7E,MAAM,UAAU,UAAU;IACxB,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,MAAM,KAAK,GAAa,EAAE,CAAA;IAE1B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;QACzC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;IACvC,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;AAC1B,CAAC;AAED,MAAM,OAAO,WAAY,SAAQ,SAAS;IACvB,IAAI,CAAM;IAE3B,YAAmB,IAAU;QAC3B,KAAK,EAAE,CAAA;QAEP,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;IAEkB,KAAK,CAAC,IAAI;QAC3B,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAA;QAEnE,MAAM,WAAW,CAAC,OAAO,EAAE,CAAA;QAE3B,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IAC3B,CAAC;CACF;AAED,SAAS,OAAO;IACd,OAAO,WAAW,CAAC,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAA;AAC1F,CAAC;AAOD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Component, Connector, Context, Locator } from '@toa.io/core';
|
|
2
|
+
import type { Declaration } from '@toa.io/definitions/extensions.continuity';
|
|
3
|
+
import type { extensions } from '@toa.io/core/types';
|
|
4
|
+
export declare class Factory implements extensions.Factory {
|
|
5
|
+
private readonly host;
|
|
6
|
+
/** what each component declared, by its locator: the hooks that wrap it are handed no more */
|
|
7
|
+
private readonly declared;
|
|
8
|
+
private journal?;
|
|
9
|
+
constructor(host: extensions.Host);
|
|
10
|
+
/**
|
|
11
|
+
* Nothing is put on the context: this is where a component's declaration is handed over, ahead
|
|
12
|
+
* of the context and the component being built.
|
|
13
|
+
*/
|
|
14
|
+
aspect(locator: Locator, declaration: Declaration): extensions.Aspect[];
|
|
15
|
+
component(component: Component): Component;
|
|
16
|
+
context(context: Context): Context;
|
|
17
|
+
service(): Connector;
|
|
18
|
+
/** one journal for the process, which the component it serves goes down with */
|
|
19
|
+
private connected;
|
|
20
|
+
}
|
|
21
|
+
export type Host = extensions.Host;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { exceptions } from '@toa.io/core';
|
|
2
|
+
import { NAMESPACE } from '@toa.io/definitions/extensions.continuity';
|
|
3
|
+
import { Composition } from './Composition.js';
|
|
4
|
+
import { Journal } from './Journal.js';
|
|
5
|
+
import { Run } from './Run.js';
|
|
6
|
+
export class Factory {
|
|
7
|
+
host;
|
|
8
|
+
/** what each component declared, by its locator: the hooks that wrap it are handed no more */
|
|
9
|
+
declared = {};
|
|
10
|
+
journal;
|
|
11
|
+
constructor(host) {
|
|
12
|
+
this.host = host;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Nothing is put on the context: this is where a component's declaration is handed over, ahead
|
|
16
|
+
* of the context and the component being built.
|
|
17
|
+
*/
|
|
18
|
+
aspect(locator, declaration) {
|
|
19
|
+
if (!mine(locator))
|
|
20
|
+
this.declared[locator.id] = declaration;
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
component(component) {
|
|
24
|
+
const declaration = this.declared[component.locator.id];
|
|
25
|
+
if (declaration === undefined)
|
|
26
|
+
return component;
|
|
27
|
+
const journal = this.connected(component);
|
|
28
|
+
const invoke = component.invoke.bind(component);
|
|
29
|
+
component.invoke = async (endpoint, request, options) => {
|
|
30
|
+
const window = declaration[endpoint];
|
|
31
|
+
if (window === undefined)
|
|
32
|
+
return await invoke(endpoint, request, options);
|
|
33
|
+
const refusal = refuse(component, endpoint, request);
|
|
34
|
+
if (refusal !== undefined)
|
|
35
|
+
return { exception: refusal };
|
|
36
|
+
let run;
|
|
37
|
+
try {
|
|
38
|
+
run = await Run.begin(request.id, journal, window);
|
|
39
|
+
}
|
|
40
|
+
catch (exception) {
|
|
41
|
+
return { exception: coded(exception) };
|
|
42
|
+
}
|
|
43
|
+
const reply = await run.within(async () => await invoke(endpoint, request, options));
|
|
44
|
+
// what was taken without waiting is kept before the attempt is done with
|
|
45
|
+
try {
|
|
46
|
+
await run.settle();
|
|
47
|
+
}
|
|
48
|
+
catch (exception) {
|
|
49
|
+
return { exception: coded(exception) };
|
|
50
|
+
}
|
|
51
|
+
return reply;
|
|
52
|
+
};
|
|
53
|
+
return component;
|
|
54
|
+
}
|
|
55
|
+
context(context) {
|
|
56
|
+
if (context.locator === undefined || this.declared[context.locator.id] === undefined)
|
|
57
|
+
return context;
|
|
58
|
+
continued(context);
|
|
59
|
+
return context;
|
|
60
|
+
}
|
|
61
|
+
// nothing connects to this, so a halt takes the whole of it
|
|
62
|
+
service() {
|
|
63
|
+
return this.host.gate(async () => new Composition(this.host));
|
|
64
|
+
}
|
|
65
|
+
/** one journal for the process, which the component it serves goes down with */
|
|
66
|
+
connected(component) {
|
|
67
|
+
if (this.journal === undefined || this.journal.disposed)
|
|
68
|
+
this.journal = new Journal(this.host);
|
|
69
|
+
component.depends(this.journal);
|
|
70
|
+
return this.journal;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The members of the context that answer an operation, each made a step of the run where there is
|
|
75
|
+
* one, and left alone where there is none. What is no step — a log, a span, a metric, the
|
|
76
|
+
* configuration, a process's own state and what its replicas decide — is not touched.
|
|
77
|
+
*/
|
|
78
|
+
function continued(context) {
|
|
79
|
+
const call = context.call.bind(context);
|
|
80
|
+
const apply = context.apply.bind(context);
|
|
81
|
+
const newid = context.newid.bind(context);
|
|
82
|
+
const now = context.now.bind(context);
|
|
83
|
+
const random = context.random.bind(context);
|
|
84
|
+
// eslint-disable-next-line max-params
|
|
85
|
+
context.call = async (namespace, name, endpoint, request, options) => {
|
|
86
|
+
const run = Run.current();
|
|
87
|
+
if (run === undefined)
|
|
88
|
+
return await call(namespace, name, endpoint, request, options);
|
|
89
|
+
return await run.step(`call:${namespace}.${name}.${endpoint}`, [request], (id) => call(namespace, name, endpoint, identified(request, id), options));
|
|
90
|
+
};
|
|
91
|
+
context.apply = async (endpoint, request, options) => {
|
|
92
|
+
const run = Run.current();
|
|
93
|
+
if (run === undefined)
|
|
94
|
+
return await apply(endpoint, request, options);
|
|
95
|
+
return await run.step(`apply:${endpoint}`, [request], (id) => apply(endpoint, identified(request, id), options));
|
|
96
|
+
};
|
|
97
|
+
context.newid = () => Run.current()?.value('newid', newid) ?? newid();
|
|
98
|
+
context.now = () => Run.current()?.value('now', now) ?? now();
|
|
99
|
+
context.random = () => Run.current()?.value('random', random) ?? random();
|
|
100
|
+
const aspects = context.aspects;
|
|
101
|
+
for (let i = 0; i < aspects.length; i++) {
|
|
102
|
+
const aspect = aspects[i];
|
|
103
|
+
if (PASSED.has(aspect.name))
|
|
104
|
+
continue;
|
|
105
|
+
const invoke = aspect.invoke.bind(aspect);
|
|
106
|
+
// what the bridge reads of an aspect is its name and `invoke`, and the aspect itself stays a
|
|
107
|
+
// dependency of the context as it was
|
|
108
|
+
aspects[i] = {
|
|
109
|
+
name: aspect.name,
|
|
110
|
+
invoke: (...args) => {
|
|
111
|
+
const run = Run.current();
|
|
112
|
+
if (run === undefined)
|
|
113
|
+
return invoke(...args);
|
|
114
|
+
return run.step(`aspect:${aspect.name}`, args, async () => await invoke(...args));
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A call made as a step carries the identity its key derives, so that it arrives under the same one
|
|
121
|
+
* on every attempt. The request is copied rather than written to: one request object may be handed
|
|
122
|
+
* to more than one call.
|
|
123
|
+
*/
|
|
124
|
+
function identified(request, id) {
|
|
125
|
+
return { ...request, id: request?.id ?? id };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A run is known by the identity a task or an event carries on every attempt. A caller that waits
|
|
129
|
+
* retries by itself, under an identity of its own, and a run of it would not be one run.
|
|
130
|
+
*/
|
|
131
|
+
function refuse(component, endpoint, request) {
|
|
132
|
+
const target = `'${component.locator.id}.${endpoint}'`;
|
|
133
|
+
if (request?.task !== true && !delivered(request))
|
|
134
|
+
return new exceptions.RequestContractException(`${target} is continued, and runs as a task or for an event, which nobody waits for`);
|
|
135
|
+
if (typeof request.id !== 'string' || !IDENTITY.test(request.id))
|
|
136
|
+
return new exceptions.RequestContractException(`${target} is continued, and a request that carries no identity has no run to continue`);
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
/** whether a receiver handed this request on, which is the last hop its chain names */
|
|
140
|
+
function delivered(request) {
|
|
141
|
+
const hops = request?.trail;
|
|
142
|
+
return Array.isArray(hops) && hops.at(-1)?.startsWith('~') === true;
|
|
143
|
+
}
|
|
144
|
+
function coded(exception) {
|
|
145
|
+
if (typeof exception?.code === 'number')
|
|
146
|
+
return exception;
|
|
147
|
+
return new exceptions.SystemException(exception);
|
|
148
|
+
}
|
|
149
|
+
/** Whether this is a component the extension ships rather than one of the application's. */
|
|
150
|
+
function mine(locator) {
|
|
151
|
+
return locator.namespace === NAMESPACE;
|
|
152
|
+
}
|
|
153
|
+
const PASSED = new Set(['logs', 'span', 'metrics', 'configuration', 'state', 'atom']);
|
|
154
|
+
// what a run is looked up by in the journal's criteria, so nothing it could hold is quoted
|
|
155
|
+
const IDENTITY = /^[0-9A-Za-z_-]{1,128}$/;
|
|
156
|
+
//# sourceMappingURL=Factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Factory.js","sourceRoot":"","sources":["../source/Factory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Connector } from '@toa.io/core';
|
|
2
|
+
import type { extensions } from '@toa.io/core/types';
|
|
3
|
+
import type { Kept } from './answers.ts';
|
|
4
|
+
/**
|
|
5
|
+
* The component that keeps the runs, as something to call.
|
|
6
|
+
*
|
|
7
|
+
* Resolved on the first call and not before: an extension is built ahead of the components of its
|
|
8
|
+
* composition, so at that point there is nothing to look up yet.
|
|
9
|
+
*/
|
|
10
|
+
export declare class Journal extends Connector {
|
|
11
|
+
private readonly host;
|
|
12
|
+
private remote?;
|
|
13
|
+
constructor(host: extensions.Host);
|
|
14
|
+
/** what a run has kept so far, by the key of each step, and when the run is reaped */
|
|
15
|
+
recall(run: string): Promise<Recalled>;
|
|
16
|
+
/** keeps what a step answered, unless another attempt kept it first, and answers what is kept */
|
|
17
|
+
record(run: string, key: string, answer: Kept, expires: number): Promise<Kept>;
|
|
18
|
+
private invoke;
|
|
19
|
+
private locate;
|
|
20
|
+
}
|
|
21
|
+
export interface Recalled {
|
|
22
|
+
steps: Map<string, Kept>;
|
|
23
|
+
expires: number | undefined;
|
|
24
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Connector, Locator } from '@toa.io/core';
|
|
2
|
+
import { COMPONENT, NAMESPACE } from '@toa.io/definitions/extensions.continuity';
|
|
3
|
+
/**
|
|
4
|
+
* The component that keeps the runs, as something to call.
|
|
5
|
+
*
|
|
6
|
+
* Resolved on the first call and not before: an extension is built ahead of the components of its
|
|
7
|
+
* composition, so at that point there is nothing to look up yet.
|
|
8
|
+
*/
|
|
9
|
+
export class Journal extends Connector {
|
|
10
|
+
host;
|
|
11
|
+
remote;
|
|
12
|
+
constructor(host) {
|
|
13
|
+
super();
|
|
14
|
+
this.host = host;
|
|
15
|
+
}
|
|
16
|
+
/** what a run has kept so far, by the key of each step, and when the run is reaped */
|
|
17
|
+
async recall(run) {
|
|
18
|
+
const steps = new Map();
|
|
19
|
+
let expires;
|
|
20
|
+
for (let omit = 0;; omit += PAGE) {
|
|
21
|
+
const page = (await this.invoke('recall', {
|
|
22
|
+
query: { criteria: `run=="${run}"`, limit: PAGE, omit }
|
|
23
|
+
}));
|
|
24
|
+
for (const record of page) {
|
|
25
|
+
steps.set(record.id, record.answer);
|
|
26
|
+
expires = record.expires;
|
|
27
|
+
}
|
|
28
|
+
if (page.length < PAGE)
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
return { steps, expires };
|
|
32
|
+
}
|
|
33
|
+
/** keeps what a step answered, unless another attempt kept it first, and answers what is kept */
|
|
34
|
+
// eslint-disable-next-line max-params
|
|
35
|
+
async record(run, key, answer, expires) {
|
|
36
|
+
return (await this.invoke('record', {
|
|
37
|
+
query: { id: key },
|
|
38
|
+
input: { run, answer, expires }
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
async invoke(endpoint, request) {
|
|
42
|
+
this.remote ??= this.locate();
|
|
43
|
+
// what the journal refuses or fails with is thrown, as for any call
|
|
44
|
+
return await (await this.remote).invoke(endpoint, request);
|
|
45
|
+
}
|
|
46
|
+
async locate() {
|
|
47
|
+
const remote = await this.host.remote(new Locator(COMPONENT, NAMESPACE), SOURCE);
|
|
48
|
+
this.depends(remote);
|
|
49
|
+
await remote.connect();
|
|
50
|
+
return remote;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** records one read brings back */
|
|
54
|
+
const PAGE = 1000;
|
|
55
|
+
// what the journal is called by; without it the calls arrive unattributed
|
|
56
|
+
const SOURCE = { service: 'continuity' };
|
|
57
|
+
//# sourceMappingURL=Journal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Journal.js","sourceRoot":"","sources":["../source/Journal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,2CAA2C,CAAA;AAKhF;;;;;GAKG;AACH,MAAM,OAAO,OAAQ,SAAQ,SAAS;IACnB,IAAI,CAAiB;IAC9B,MAAM,CAAkB;IAEhC,YAAmB,IAAqB;QACtC,KAAK,EAAE,CAAA;QAEP,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;IAED,sFAAsF;IAC/E,KAAK,CAAC,MAAM,CAAC,GAAW;QAC7B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAgB,CAAA;QACrC,IAAI,OAA2B,CAAA;QAE/B,KAAK,IAAI,IAAI,GAAG,CAAC,GAAI,IAAI,IAAI,IAAI,EAAE,CAAC;YAClC,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE;gBACxC,KAAK,EAAE,EAAE,QAAQ,EAAE,SAAS,GAAG,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE;aACxD,CAAC,CAAa,CAAA;YAEf,KAAK,MAAM,MAAM,IAAI,IAAI,EAAE,CAAC;gBAC1B,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;gBACnC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;YAC1B,CAAC;YAED,IAAI,IAAI,CAAC,MAAM,GAAG,IAAI;gBAAE,MAAK;QAC/B,CAAC;QAED,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;IAC3B,CAAC;IAED,iGAAiG;IACjG,sCAAsC;IAC/B,KAAK,CAAC,MAAM,CACjB,GAAW,EACX,GAAW,EACX,MAAY,EACZ,OAAe;QAEf,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE;YAClC,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE;YAClB,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE;SAChC,CAAC,CAAS,CAAA;IACb,CAAC;IAEO,KAAK,CAAC,MAAM,CAAC,QAAgB,EAAE,OAAe;QACpD,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,EAAE,CAAA;QAE7B,oEAAoE;QACpE,OAAO,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IAC5D,CAAC;IAEO,KAAK,CAAC,MAAM;QAClB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,CAAA;QAEhF,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QAEpB,MAAM,MAAM,CAAC,OAAO,EAAE,CAAA;QAEtB,OAAO,MAAM,CAAA;IACf,CAAC;CACF;AAaD,mCAAmC;AACnC,MAAM,IAAI,GAAG,IAAI,CAAA;AAEjB,0EAA0E;AAC1E,MAAM,MAAM,GAAW,EAAE,OAAO,EAAE,YAAY,EAAE,CAAA"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Journal } from './Journal.ts';
|
|
2
|
+
/**
|
|
3
|
+
* One attempt of a run: a task or an event, known by the identity it carries on every attempt.
|
|
4
|
+
*
|
|
5
|
+
* Put in scope where the component is invoked, and read where the context is asked. The context
|
|
6
|
+
* of an operation is one object shared by every invocation of it, so nothing per invocation can
|
|
7
|
+
* be handed to it — as the trail is not, for the same reason.
|
|
8
|
+
*/
|
|
9
|
+
export declare class Run {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
private readonly journal;
|
|
12
|
+
private readonly window;
|
|
13
|
+
private readonly steps;
|
|
14
|
+
private expires;
|
|
15
|
+
/** how many times this attempt has asked each thing, by what it asked */
|
|
16
|
+
private readonly asked;
|
|
17
|
+
/** what was taken without waiting and is still being written */
|
|
18
|
+
private readonly writing;
|
|
19
|
+
private constructor();
|
|
20
|
+
/** an attempt, with what the run kept before it */
|
|
21
|
+
static begin(id: string, journal: Journal, window: number): Promise<Run>;
|
|
22
|
+
/**
|
|
23
|
+
* The run the invocation running now is, where it is one. Only the invocation the run was
|
|
24
|
+
* handed to: a call it makes to its own component runs in the same scope, and under an identity
|
|
25
|
+
* of its own.
|
|
26
|
+
*/
|
|
27
|
+
static current(): Run | undefined;
|
|
28
|
+
/** runs `attempt` as this run */
|
|
29
|
+
within<T>(attempt: () => Promise<T>): Promise<T>;
|
|
30
|
+
/**
|
|
31
|
+
* A step that answers later: what was kept is given back, and what was not is made — under the
|
|
32
|
+
* identity the step's key is — kept, and what is kept is given back. So the operation is given
|
|
33
|
+
* what a later attempt will be given, including where a copy of the run kept it first.
|
|
34
|
+
*/
|
|
35
|
+
step(member: string, args: unknown[], make: (id: string) => Promise<unknown>): Promise<unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* A step that answers at once — a new id, the time, a random number. What was kept is given
|
|
38
|
+
* back; what was not is taken, given, and written without waiting, and anything the attempt makes
|
|
39
|
+
* after it waits for the write.
|
|
40
|
+
*/
|
|
41
|
+
value<T>(member: string, take: () => T): T;
|
|
42
|
+
/** waits for what is being written, and raises where a write failed */
|
|
43
|
+
settle(): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* The member, a digest of what it was asked, and how many times this attempt has asked exactly
|
|
46
|
+
* that before: the order steps are made in is not the same on every attempt, and what they ask
|
|
47
|
+
* is.
|
|
48
|
+
*/
|
|
49
|
+
private key;
|
|
50
|
+
/** fixed by the run's first step, so that a run is kept whole and reaped whole */
|
|
51
|
+
private expiry;
|
|
52
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { entities, trail } from '@toa.io/core';
|
|
3
|
+
import { decode, encode } from './answers.js';
|
|
4
|
+
import { fingerprint } from './fingerprint.js';
|
|
5
|
+
/**
|
|
6
|
+
* One attempt of a run: a task or an event, known by the identity it carries on every attempt.
|
|
7
|
+
*
|
|
8
|
+
* Put in scope where the component is invoked, and read where the context is asked. The context
|
|
9
|
+
* of an operation is one object shared by every invocation of it, so nothing per invocation can
|
|
10
|
+
* be handed to it — as the trail is not, for the same reason.
|
|
11
|
+
*/
|
|
12
|
+
export class Run {
|
|
13
|
+
id;
|
|
14
|
+
journal;
|
|
15
|
+
window;
|
|
16
|
+
steps;
|
|
17
|
+
expires;
|
|
18
|
+
/** how many times this attempt has asked each thing, by what it asked */
|
|
19
|
+
asked = new Map();
|
|
20
|
+
/** what was taken without waiting and is still being written */
|
|
21
|
+
writing = new Set();
|
|
22
|
+
// eslint-disable-next-line max-params
|
|
23
|
+
constructor(id, journal, window, recalled) {
|
|
24
|
+
this.id = id;
|
|
25
|
+
this.journal = journal;
|
|
26
|
+
this.window = window;
|
|
27
|
+
this.steps = recalled.steps;
|
|
28
|
+
this.expires = recalled.expires;
|
|
29
|
+
}
|
|
30
|
+
/** an attempt, with what the run kept before it */
|
|
31
|
+
static async begin(id, journal, window) {
|
|
32
|
+
return new Run(id, journal, window, await journal.recall(id));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The run the invocation running now is, where it is one. Only the invocation the run was
|
|
36
|
+
* handed to: a call it makes to its own component runs in the same scope, and under an identity
|
|
37
|
+
* of its own.
|
|
38
|
+
*/
|
|
39
|
+
static current() {
|
|
40
|
+
const run = storage.getStore();
|
|
41
|
+
if (run === undefined || trail.current()?.id !== run.id)
|
|
42
|
+
return undefined;
|
|
43
|
+
return run;
|
|
44
|
+
}
|
|
45
|
+
/** runs `attempt` as this run */
|
|
46
|
+
async within(attempt) {
|
|
47
|
+
return await storage.run(this, attempt);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A step that answers later: what was kept is given back, and what was not is made — under the
|
|
51
|
+
* identity the step's key is — kept, and what is kept is given back. So the operation is given
|
|
52
|
+
* what a later attempt will be given, including where a copy of the run kept it first.
|
|
53
|
+
*/
|
|
54
|
+
async step(member, args, make) {
|
|
55
|
+
const key = this.key(member, args);
|
|
56
|
+
const kept = this.steps.get(key);
|
|
57
|
+
if (kept !== undefined)
|
|
58
|
+
return decode(kept);
|
|
59
|
+
// a value taken without waiting may be what this step is asked with, so it is kept first
|
|
60
|
+
await this.settle();
|
|
61
|
+
const answer = await make(key);
|
|
62
|
+
const stored = await this.journal.record(this.id, key, await encode(answer), this.expiry());
|
|
63
|
+
this.steps.set(key, stored);
|
|
64
|
+
return decode(stored);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A step that answers at once — a new id, the time, a random number. What was kept is given
|
|
68
|
+
* back; what was not is taken, given, and written without waiting, and anything the attempt makes
|
|
69
|
+
* after it waits for the write.
|
|
70
|
+
*/
|
|
71
|
+
value(member, take) {
|
|
72
|
+
const key = this.key(member, []);
|
|
73
|
+
const kept = this.steps.get(key);
|
|
74
|
+
if (kept !== undefined)
|
|
75
|
+
return decode(kept);
|
|
76
|
+
const value = take();
|
|
77
|
+
const encoded = { value };
|
|
78
|
+
this.steps.set(key, encoded);
|
|
79
|
+
const write = this.journal.record(this.id, key, encoded, this.expiry());
|
|
80
|
+
this.writing.add(write);
|
|
81
|
+
void write.then(() => this.writing.delete(write), () => undefined);
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
/** waits for what is being written, and raises where a write failed */
|
|
85
|
+
async settle() {
|
|
86
|
+
if (this.writing.size > 0)
|
|
87
|
+
await Promise.all(this.writing);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The member, a digest of what it was asked, and how many times this attempt has asked exactly
|
|
91
|
+
* that before: the order steps are made in is not the same on every attempt, and what they ask
|
|
92
|
+
* is.
|
|
93
|
+
*/
|
|
94
|
+
key(member, args) {
|
|
95
|
+
const asked = member + ':' + fingerprint(args);
|
|
96
|
+
const count = this.asked.get(asked) ?? 0;
|
|
97
|
+
this.asked.set(asked, count + 1);
|
|
98
|
+
return entities.derive(this.id, asked, count);
|
|
99
|
+
}
|
|
100
|
+
/** fixed by the run's first step, so that a run is kept whole and reaped whole */
|
|
101
|
+
expiry() {
|
|
102
|
+
this.expires ??= Date.now() + this.window * 1000;
|
|
103
|
+
return this.expires;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// as the trail holds its own: a process may carry two copies of this module
|
|
107
|
+
const KEY = Symbol.for('toa.continuity.run');
|
|
108
|
+
const storage = (globalThis[KEY] ??= new AsyncLocalStorage());
|
|
109
|
+
//# sourceMappingURL=Run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Run.js","sourceRoot":"","sources":["../source/Run.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a step answered, in the form it is kept in: something the journal's storage holds and the
|
|
3
|
+
* broker carries as it is, and that gives back what the operation was given.
|
|
4
|
+
*/
|
|
5
|
+
export type Kept = {
|
|
6
|
+
value: unknown;
|
|
7
|
+
} | {
|
|
8
|
+
none: true;
|
|
9
|
+
} | {
|
|
10
|
+
bytes: string;
|
|
11
|
+
} | {
|
|
12
|
+
error: Record<string, unknown>;
|
|
13
|
+
} | {
|
|
14
|
+
response: KeptResponse;
|
|
15
|
+
};
|
|
16
|
+
interface KeptResponse {
|
|
17
|
+
status: number;
|
|
18
|
+
statusText: string;
|
|
19
|
+
headers: Array<[string, string]>;
|
|
20
|
+
body: string | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A stream cannot be kept: it is read once, and what it would give back is whatever is left of it.
|
|
24
|
+
* A `Response` is the exception, because what a `fetch` answers with is one, and it is read whole
|
|
25
|
+
* to be kept.
|
|
26
|
+
*/
|
|
27
|
+
export declare function encode(answer: unknown): Promise<Kept>;
|
|
28
|
+
export declare function decode(kept: Kept): unknown;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { exceptions } from '@toa.io/core';
|
|
2
|
+
/**
|
|
3
|
+
* A stream cannot be kept: it is read once, and what it would give back is whatever is left of it.
|
|
4
|
+
* A `Response` is the exception, because what a `fetch` answers with is one, and it is read whole
|
|
5
|
+
* to be kept.
|
|
6
|
+
*/
|
|
7
|
+
export async function encode(answer) {
|
|
8
|
+
if (answer === undefined)
|
|
9
|
+
return { none: true };
|
|
10
|
+
if (answer instanceof Response)
|
|
11
|
+
return { response: await response(answer) };
|
|
12
|
+
if (streamed(answer))
|
|
13
|
+
throw new exceptions.UnrecordableException(`a step answered with a stream, which is read once and cannot be given back`);
|
|
14
|
+
if (ArrayBuffer.isView(answer))
|
|
15
|
+
return {
|
|
16
|
+
bytes: Buffer.from(answer.buffer, answer.byteOffset, answer.byteLength).toString('base64')
|
|
17
|
+
};
|
|
18
|
+
// a declared error a call answered with: its own properties are what the caller reads
|
|
19
|
+
if (answer instanceof Error)
|
|
20
|
+
return { error: { ...answer } };
|
|
21
|
+
return { value: answer };
|
|
22
|
+
}
|
|
23
|
+
export function decode(kept) {
|
|
24
|
+
if ('none' in kept)
|
|
25
|
+
return undefined;
|
|
26
|
+
if ('bytes' in kept)
|
|
27
|
+
return Buffer.from(kept.bytes, 'base64');
|
|
28
|
+
if ('error' in kept)
|
|
29
|
+
return Object.assign(new Error(), kept.error);
|
|
30
|
+
if ('response' in kept)
|
|
31
|
+
return revive(kept.response);
|
|
32
|
+
return kept.value;
|
|
33
|
+
}
|
|
34
|
+
async function response(answer) {
|
|
35
|
+
const body = answer.body === null
|
|
36
|
+
? null
|
|
37
|
+
: Buffer.from(await answer.arrayBuffer()).toString('base64');
|
|
38
|
+
return {
|
|
39
|
+
status: answer.status,
|
|
40
|
+
statusText: answer.statusText,
|
|
41
|
+
headers: [...answer.headers.entries()],
|
|
42
|
+
body
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function revive(kept) {
|
|
46
|
+
const body = kept.body === null ? null : Buffer.from(kept.body, 'base64');
|
|
47
|
+
return new Response(body, {
|
|
48
|
+
status: kept.status,
|
|
49
|
+
statusText: kept.statusText,
|
|
50
|
+
headers: kept.headers
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function streamed(value) {
|
|
54
|
+
if (value === null || typeof value !== 'object')
|
|
55
|
+
return false;
|
|
56
|
+
const object = value;
|
|
57
|
+
return (typeof object.pipe === 'function' ||
|
|
58
|
+
typeof object.getReader === 'function' ||
|
|
59
|
+
typeof object[Symbol.asyncIterator] === 'function');
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=answers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"answers.js","sourceRoot":"","sources":["../source/answers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAoBzC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,MAAe;IAC1C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;IAE/C,IAAI,MAAM,YAAY,QAAQ;QAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAA;IAE3E,IAAI,QAAQ,CAAC,MAAM,CAAC;QAClB,MAAM,IAAI,UAAU,CAAC,qBAAqB,CACxC,4EAA4E,CAC7E,CAAA;IAEH,IAAI,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC;QAC5B,OAAO;YACL,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,QAAQ,CAC9E,QAAQ,CACT;SACF,CAAA;IAEH,sFAAsF;IACtF,IAAI,MAAM,YAAY,KAAK;QAAE,OAAO,EAAE,KAAK,EAAE,EAAE,GAAG,MAAM,EAAE,EAAE,CAAA;IAE5D,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAA;AAC1B,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAU;IAC/B,IAAI,MAAM,IAAI,IAAI;QAAE,OAAO,SAAS,CAAA;IACpC,IAAI,OAAO,IAAI,IAAI;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;IAC7D,IAAI,OAAO,IAAI,IAAI;QAAE,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;IAClE,IAAI,UAAU,IAAI,IAAI;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAEpD,OAAO,IAAI,CAAC,KAAK,CAAA;AACnB,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,MAAgB;IACtC,MAAM,IAAI,GACR,MAAM,CAAC,IAAI,KAAK,IAAI;QAClB,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;IAEhE,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,OAAO,EAAE,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACtC,IAAI;KACL,CAAA;AACH,CAAC;AAED,SAAS,MAAM,CAAC,IAAkB;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;IAEzE,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;QACxB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAE7D,MAAM,MAAM,GAAG,KAAyC,CAAA;IAExD,OAAO,CACL,OAAO,MAAM,CAAC,IAAI,KAAK,UAAU;QACjC,OAAO,MAAM,CAAC,SAAS,KAAK,UAAU;QACtC,OAAO,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,UAAU,CACnD,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a step asks, as a digest: the same arguments give the same one on every attempt, whatever
|
|
3
|
+
* order an object's keys were written in.
|
|
4
|
+
*
|
|
5
|
+
* Only what a step is asked is read. A value no attempt can give twice — a function, a signal, a
|
|
6
|
+
* stream — is told by its kind alone, so it neither tells two attempts apart nor fails to digest.
|
|
7
|
+
*/
|
|
8
|
+
export declare function fingerprint(args: unknown[]): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* What a step asks, as a digest: the same arguments give the same one on every attempt, whatever
|
|
4
|
+
* order an object's keys were written in.
|
|
5
|
+
*
|
|
6
|
+
* Only what a step is asked is read. A value no attempt can give twice — a function, a signal, a
|
|
7
|
+
* stream — is told by its kind alone, so it neither tells two attempts apart nor fails to digest.
|
|
8
|
+
*/
|
|
9
|
+
export function fingerprint(args) {
|
|
10
|
+
const hash = createHash('sha1');
|
|
11
|
+
write(hash, args, new Set());
|
|
12
|
+
return hash.digest('hex');
|
|
13
|
+
}
|
|
14
|
+
function write(hash, value, seen) {
|
|
15
|
+
if (value === null || typeof value !== 'object') {
|
|
16
|
+
// the type as well, so that `1` and `'1'` are two different asks
|
|
17
|
+
hash.update(typeof value + ':' + String(value) + ';');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (seen.has(value)) {
|
|
21
|
+
hash.update('cycle;');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
seen.add(value);
|
|
25
|
+
if (ArrayBuffer.isView(value)) {
|
|
26
|
+
hash.update('bytes:');
|
|
27
|
+
hash.update(value);
|
|
28
|
+
hash.update(';');
|
|
29
|
+
}
|
|
30
|
+
else if (value instanceof Date)
|
|
31
|
+
hash.update('date:' + value.toISOString() + ';');
|
|
32
|
+
else if (value instanceof URL)
|
|
33
|
+
hash.update('url:' + value.href + ';');
|
|
34
|
+
else if (Array.isArray(value)) {
|
|
35
|
+
hash.update('[');
|
|
36
|
+
for (const item of value)
|
|
37
|
+
write(hash, item, seen);
|
|
38
|
+
hash.update(']');
|
|
39
|
+
}
|
|
40
|
+
else if (plain(value)) {
|
|
41
|
+
hash.update('{');
|
|
42
|
+
for (const key of Object.keys(value).sort()) {
|
|
43
|
+
const item = value[key];
|
|
44
|
+
// absent and undefined are one ask, as they are once the request is on the wire
|
|
45
|
+
if (item === undefined)
|
|
46
|
+
continue;
|
|
47
|
+
hash.update(JSON.stringify(key) + '=');
|
|
48
|
+
write(hash, item, seen);
|
|
49
|
+
}
|
|
50
|
+
hash.update('}');
|
|
51
|
+
}
|
|
52
|
+
else
|
|
53
|
+
hash.update('<' + (value.constructor?.name ?? 'object') + '>;');
|
|
54
|
+
seen.delete(value);
|
|
55
|
+
}
|
|
56
|
+
function plain(value) {
|
|
57
|
+
const prototype = Object.getPrototypeOf(value);
|
|
58
|
+
return prototype === Object.prototype || prototype === null;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=fingerprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../source/fingerprint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,IAAe;IACzC,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAA;IAE/B,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,GAAG,EAAE,CAAC,CAAA;IAE5B,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC3B,CAAC;AAID,SAAS,KAAK,CAAC,IAAU,EAAE,KAAc,EAAE,IAAiB;IAC1D,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,iEAAiE;QACjE,IAAI,CAAC,MAAM,CAAC,OAAO,KAAK,GAAG,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAA;QAErD,OAAM;IACR,CAAC;IAED,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QACpB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;QAErB,OAAM;IACR,CAAC;IAED,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IAEf,IAAI,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;QACrB,IAAI,CAAC,MAAM,CAAC,KAAmB,CAAC,CAAA;QAChC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAClB,CAAC;SAAM,IAAI,KAAK,YAAY,IAAI;QAAE,IAAI,CAAC,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC,CAAA;SAC7E,IAAI,KAAK,YAAY,GAAG;QAAE,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC,CAAA;SAChE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAEhB,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;QAEjD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAClB,CAAC;SAAM,IAAI,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QACxB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAEhB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAI,KAAiC,CAAC,GAAG,CAAC,CAAA;YAEpD,gFAAgF;YAChF,IAAI,IAAI,KAAK,SAAS;gBAAE,SAAQ;YAEhC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAA;YACtC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;QACzB,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAClB,CAAC;;QAAM,IAAI,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,IAAI,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAA;IAEtE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACpB,CAAC;AAED,SAAS,KAAK,CAAC,KAAa;IAC1B,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAA;IAE9C,OAAO,SAAS,KAAK,MAAM,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI,CAAA;AAC7D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../source/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA"}
|