@toa.io/extensions.exposition 1.0.0-alpha.271 → 1.0.0-alpha.273
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/CHANGELOG.md +40 -0
- package/components/{exposition.stash → exposition.atom}/manifest.toa.yaml +4 -4
- package/components/{exposition.stash → exposition.atom}/operations/meter.js +1 -1
- package/components/exposition.octets/manifest.toa.yaml +28 -10
- package/components/exposition.octets/operations/put.js +20 -5
- package/components/identity.bans/manifest.toa.yaml +26 -7
- package/components/identity.bans/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.basic/manifest.toa.yaml +106 -30
- package/components/identity.basic/operations/authenticate.js +6 -3
- package/components/identity.basic/operations/authenticate.js.map +1 -1
- package/components/identity.basic/operations/incept.js +3 -2
- package/components/identity.basic/operations/incept.js.map +1 -1
- package/components/identity.basic/operations/transit.js +16 -5
- package/components/identity.basic/operations/transit.js.map +1 -1
- package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.basic/source/authenticate.ts +7 -3
- package/components/identity.basic/source/incept.ts +3 -2
- package/components/identity.basic/source/transit.ts +19 -5
- package/components/identity.credentials/manifest.toa.yaml +34 -13
- package/components/identity.credentials/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/manifest.toa.yaml +120 -28
- package/components/identity.federation/operations/authenticate.js +3 -2
- package/components/identity.federation/operations/authenticate.js.map +1 -1
- package/components/identity.federation/operations/create.js +4 -2
- package/components/identity.federation/operations/create.js.map +1 -1
- package/components/identity.federation/operations/lib/errors.d.ts +70 -11
- package/components/identity.federation/operations/lib/errors.js +40 -11
- package/components/identity.federation/operations/lib/errors.js.map +1 -1
- package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/source/authenticate.ts +3 -2
- package/components/identity.federation/source/create.ts +4 -2
- package/components/identity.federation/source/lib/errors.ts +49 -12
- package/components/identity.keys/manifest.toa.yaml +14 -6
- package/components/identity.keys/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.otp/manifest.toa.yaml +44 -15
- package/components/identity.otp/operations/authenticate.js +9 -4
- package/components/identity.otp/operations/authenticate.js.map +1 -1
- package/components/identity.otp/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.otp/source/authenticate.ts +11 -4
- package/components/identity.passkeys/manifest.toa.yaml +38 -8
- package/components/identity.passkeys/operations/authenticate.js +3 -2
- package/components/identity.passkeys/operations/authenticate.js.map +1 -1
- package/components/identity.passkeys/operations/create.js +6 -3
- package/components/identity.passkeys/operations/create.js.map +1 -1
- package/components/identity.passkeys/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.passkeys/operations/use.js +6 -3
- package/components/identity.passkeys/operations/use.js.map +1 -1
- package/components/identity.passkeys/source/authenticate.ts +3 -2
- package/components/identity.passkeys/source/create.ts +7 -3
- package/components/identity.passkeys/source/use.ts +7 -3
- package/components/identity.roles/manifest.toa.yaml +41 -10
- package/components/identity.roles/operations/grant.js +3 -2
- package/components/identity.roles/operations/grant.js.map +1 -1
- package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.roles/source/grant.ts +3 -2
- package/components/identity.tokens/manifest.toa.yaml +99 -39
- package/components/identity.tokens/operations/authenticate.js +6 -3
- package/components/identity.tokens/operations/authenticate.js.map +1 -1
- package/components/identity.tokens/operations/decrypt.js +9 -4
- package/components/identity.tokens/operations/decrypt.js.map +1 -1
- package/components/identity.tokens/operations/encrypt.js +3 -2
- package/components/identity.tokens/operations/encrypt.js.map +1 -1
- package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/source/authenticate.ts +7 -3
- package/components/identity.tokens/source/decrypt.ts +11 -4
- package/components/identity.tokens/source/encrypt.ts +3 -2
- package/documentation/identity.md +4 -1
- package/documentation/notes/throttling.md +1 -1
- package/features/cors.feature +1 -1
- package/features/dev.feature +1 -1
- package/features/identity.federation.feature +3 -2
- package/features/{identtiy.tokens.custom.feature → identity.tokens.custom.feature} +39 -0
- package/features/introspection.feature +27 -34
- package/features/map.feature +1 -1
- package/features/octets.download.feature +1 -1
- package/features/octets.meta.feature +1 -1
- package/features/passkeys.feature +8 -8
- package/features/query.feature +27 -0
- package/features/require.feature +2 -2
- package/features/response.feature +3 -3
- package/features/steps/Components.ts +12 -4
- package/features/steps/Database.ts +9 -0
- package/features/steps/Gateway.ts +13 -13
- package/features/steps/HTTP.ts +3 -5
- package/features/steps/Parameters.ts +2 -3
- package/features/steps/Realtime.ts +4 -4
- package/features/steps/Workspace.ts +8 -3
- package/features/steps/components/audit/manifest.toa.yaml +12 -5
- package/features/steps/components/echo/manifest.toa.yaml +33 -8
- package/features/steps/components/notify/manifest.toa.yaml +12 -5
- package/features/steps/components/octets.tester/manifest.toa.yaml +43 -14
- package/features/steps/components/orders/manifest.toa.yaml +22 -5
- package/features/steps/components/pots/manifest.toa.yaml +71 -17
- package/features/steps/components/pricing/manifest.toa.yaml +8 -2
- package/features/steps/components/sequences/manifest.toa.yaml +3 -1
- package/features/steps/components/users/manifest.toa.yaml +18 -3
- package/features/steps/components/users.properties/manifest.toa.yaml +12 -2
- package/package.json +6 -7
- package/schemas/annotation.cos.yaml +37 -17
- package/schemas/directive.cos.yaml +7 -3
- package/schemas/io/throttle.cos.yaml +47 -8
- package/schemas/method.cos.yaml +21 -9
- package/schemas/node.cos.yaml +22 -7
- package/schemas/octets/delete.cos.yaml +15 -2
- package/schemas/octets/get.cos.yaml +5 -2
- package/schemas/octets/put.cos.yaml +6 -3
- package/schemas/octets/workflow.cos.yaml +0 -1
- package/schemas/query.cos.yaml +31 -14
- package/schemas/querystring.cos.yaml +19 -7
- package/schemas/range.cos.yaml +10 -2
- package/schemas/route.cos.yaml +6 -2
- package/source/Branch.test.ts +37 -0
- package/source/Branch.ts +37 -0
- package/source/Factory.ts +5 -5
- package/source/Gateway.ts +29 -14
- package/source/HTTP/formats/yaml.test.ts +4 -2
- package/source/RTD/Tree.ts +4 -1
- package/source/Tenant.ts +15 -4
- package/source/deployment.ts +3 -4
- package/source/directives/io/IO.ts +2 -2
- package/source/directives/io/lib/throttle/Sync.ts +4 -4
- package/source/directives/map/Headers.ts +8 -1
- package/source/directives/octets/Put.ts +6 -3
- package/source/root.ts +2 -2
- package/transpiled/Branch.d.ts +23 -0
- package/transpiled/Branch.js +16 -0
- package/transpiled/Branch.js.map +1 -1
- package/transpiled/Factory.js +4 -4
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Gateway.d.ts +7 -0
- package/transpiled/Gateway.js +26 -8
- package/transpiled/Gateway.js.map +1 -1
- package/transpiled/RTD/Tree.js +4 -1
- package/transpiled/RTD/Tree.js.map +1 -1
- package/transpiled/Tenant.d.ts +8 -2
- package/transpiled/Tenant.js +12 -2
- package/transpiled/Tenant.js.map +1 -1
- package/transpiled/deployment.js +3 -4
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/directives/io/IO.d.ts +1 -1
- package/transpiled/directives/io/IO.js +2 -2
- package/transpiled/directives/io/IO.js.map +1 -1
- package/transpiled/directives/io/lib/throttle/Sync.d.ts +2 -2
- package/transpiled/directives/io/lib/throttle/Sync.js +4 -4
- package/transpiled/directives/io/lib/throttle/Sync.js.map +1 -1
- package/transpiled/directives/map/Headers.js +7 -1
- package/transpiled/directives/map/Headers.js.map +1 -1
- package/transpiled/directives/octets/Put.js.map +1 -1
- package/transpiled/root.js +1 -1
- package/transpiled/root.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
|
@@ -18,11 +18,14 @@ properties:
|
|
|
18
18
|
type: string
|
|
19
19
|
workflow:
|
|
20
20
|
anyOf:
|
|
21
|
-
-
|
|
22
|
-
type: object
|
|
21
|
+
- type: object
|
|
23
22
|
patternProperties:
|
|
24
23
|
^.+$:
|
|
25
24
|
type: string
|
|
26
25
|
- type: array
|
|
27
|
-
items:
|
|
26
|
+
items:
|
|
27
|
+
type: object
|
|
28
|
+
patternProperties:
|
|
29
|
+
^.+$:
|
|
30
|
+
type: string
|
|
28
31
|
additionalProperties: false
|
package/schemas/query.cos.yaml
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
properties:
|
|
2
|
+
id:
|
|
3
|
+
type: string
|
|
4
|
+
criteria:
|
|
5
|
+
type: string
|
|
6
|
+
sort:
|
|
7
|
+
type: string
|
|
8
|
+
search:
|
|
9
|
+
type: boolean
|
|
10
|
+
sample:
|
|
11
|
+
type: integer
|
|
12
|
+
omit:
|
|
13
|
+
$ref: range
|
|
14
|
+
limit:
|
|
15
|
+
$ref: range
|
|
16
|
+
selectors:
|
|
17
|
+
type: array
|
|
18
|
+
items:
|
|
19
|
+
type: string
|
|
20
|
+
projection:
|
|
21
|
+
type: array
|
|
22
|
+
items:
|
|
23
|
+
type: string
|
|
24
|
+
parameters:
|
|
25
|
+
type: array
|
|
26
|
+
items:
|
|
27
|
+
type: string
|
|
28
|
+
deleted:
|
|
29
|
+
type: boolean
|
|
30
|
+
nullable: true
|
|
31
|
+
type: object
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
properties:
|
|
2
|
+
id:
|
|
3
|
+
type: string
|
|
4
|
+
criteria:
|
|
5
|
+
type: string
|
|
6
|
+
search:
|
|
7
|
+
type: string
|
|
8
|
+
sample:
|
|
9
|
+
type: number
|
|
10
|
+
sort:
|
|
11
|
+
type: string
|
|
12
|
+
omit:
|
|
13
|
+
type: number
|
|
14
|
+
limit:
|
|
15
|
+
type: number
|
|
16
|
+
version:
|
|
17
|
+
type: number
|
|
18
|
+
additionalProperties: false
|
|
19
|
+
type: object
|
package/schemas/range.cos.yaml
CHANGED
package/schemas/route.cos.yaml
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { decide } from './Branch'
|
|
2
|
+
import type { Branch, Exposed } from './Branch'
|
|
3
|
+
|
|
4
|
+
function branch (version: string, timestamp: number): Branch {
|
|
5
|
+
return {
|
|
6
|
+
namespace: 'default',
|
|
7
|
+
component: 'one',
|
|
8
|
+
isolated: false,
|
|
9
|
+
node: { routes: [], methods: [], directives: [] },
|
|
10
|
+
version,
|
|
11
|
+
timestamp
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function exposed (version: string, timestamp: number): Exposed {
|
|
16
|
+
return { version, timestamp, nodes: [] }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
it('should refresh the same version', () => {
|
|
20
|
+
expect(decide(exposed('a', 1), branch('a', 2))).toStrictEqual('refresh')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should refresh the same version announced by an older tenant', () => {
|
|
24
|
+
expect(decide(exposed('a', 2), branch('a', 1))).toStrictEqual('refresh')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should merge a newer tenant', () => {
|
|
28
|
+
expect(decide(exposed('a', 1), branch('b', 2))).toStrictEqual('merge')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should not merge a tenant that started earlier', () => {
|
|
32
|
+
expect(decide(exposed('b', 2), branch('a', 1))).toStrictEqual('superseded')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should merge when tenants started at the same time', () => {
|
|
36
|
+
expect(decide(exposed('a', 1), branch('b', 1))).toStrictEqual('merge')
|
|
37
|
+
})
|
package/source/Branch.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type * as RTD from './RTD/syntax'
|
|
2
|
+
import type { Node } from './RTD'
|
|
2
3
|
|
|
3
4
|
export interface Branch {
|
|
4
5
|
namespace: string
|
|
@@ -6,4 +7,40 @@ export interface Branch {
|
|
|
6
7
|
isolated: boolean
|
|
7
8
|
node: RTD.Node
|
|
8
9
|
version: string
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* When the tenant that announced it started, by its own clock. A replica on its way
|
|
13
|
+
* out announces the same component with an older stamp than the one replacing it, and
|
|
14
|
+
* that is what tells the two apart when their versions differ.
|
|
15
|
+
*/
|
|
16
|
+
timestamp: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A branch the gateway has merged, and the tenant it came from. */
|
|
20
|
+
export interface Exposed {
|
|
21
|
+
version: string
|
|
22
|
+
|
|
23
|
+
/** The start time of the tenant this branch came from. */
|
|
24
|
+
timestamp: number
|
|
25
|
+
|
|
26
|
+
nodes: Node[]
|
|
9
27
|
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* What to do with an announcement about a component that already has a branch exposed.
|
|
31
|
+
*
|
|
32
|
+
* - `refresh`: the same version, so its expiration is extended and its endpoints left alone
|
|
33
|
+
* - `superseded`: it came from a tenant that started before the one exposed now
|
|
34
|
+
* - `merge`: everything else
|
|
35
|
+
*/
|
|
36
|
+
export function decide (exposed: Exposed, branch: Branch): Decision {
|
|
37
|
+
if (exposed.version === branch.version)
|
|
38
|
+
return 'refresh'
|
|
39
|
+
|
|
40
|
+
if (branch.timestamp < exposed.timestamp)
|
|
41
|
+
return 'superseded'
|
|
42
|
+
|
|
43
|
+
return 'merge'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type Decision = 'merge' | 'refresh' | 'superseded'
|
package/source/Factory.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
2
|
import { createHash } from 'node:crypto'
|
|
3
3
|
import { console, traces, type LevelName, type TracesOptions } from 'openspan'
|
|
4
|
-
import { decode } from '@toa.io/generic'
|
|
5
4
|
import { Tenant } from './Tenant'
|
|
6
5
|
import { Gateway } from './Gateway'
|
|
7
6
|
import { Remotes } from './Remotes'
|
|
@@ -29,7 +28,8 @@ export class Factory implements extensions.Factory {
|
|
|
29
28
|
const broadcast: Broadcast = this.boot.bindings.broadcast(CHANNEL, locator.id)
|
|
30
29
|
const hash = createHash('sha256').update(JSON.stringify(node)).digest('hex')
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
// no timestamp: the tenant stamps each announcement with its own start time
|
|
32
|
+
const branch: Omit<Branch, 'timestamp'> = {
|
|
33
33
|
namespace: locator.namespace,
|
|
34
34
|
component: locator.name,
|
|
35
35
|
isolated: locator.namespace === 'identity',
|
|
@@ -46,7 +46,7 @@ export class Factory implements extensions.Factory {
|
|
|
46
46
|
|
|
47
47
|
configureLogs()
|
|
48
48
|
|
|
49
|
-
const options =
|
|
49
|
+
const options = JSON.parse(process.env.TOA_EXPOSITION_PROPERTIES) as http.Options
|
|
50
50
|
const broadcast: Broadcast = this.boot.bindings.broadcast(CHANNEL)
|
|
51
51
|
const server = http.Server.create({ ...options })
|
|
52
52
|
const remotes = new Remotes(this.boot)
|
|
@@ -76,13 +76,13 @@ const TRACES_ENV = 'TOA_TELEMETRY_TRACES'
|
|
|
76
76
|
function configureLogs (): void {
|
|
77
77
|
const globEnv = process.env[LOGS_PREFIX]
|
|
78
78
|
const level: LevelName = process.env.TOA_DEV === '1' ? 'trace' : 'info'
|
|
79
|
-
const options = globEnv === undefined ? { level } :
|
|
79
|
+
const options = globEnv === undefined ? { level } : JSON.parse(globEnv) as { level?: LevelName }
|
|
80
80
|
|
|
81
81
|
console.configure({ level: options.level ?? level })
|
|
82
82
|
|
|
83
83
|
const tracesEnv = process.env[TRACES_ENV]
|
|
84
84
|
|
|
85
|
-
traces(tracesEnv === undefined ? development() :
|
|
85
|
+
traces(tracesEnv === undefined ? development() : JSON.parse(tracesEnv) as TracesOptions)
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
package/source/Gateway.ts
CHANGED
|
@@ -4,10 +4,11 @@ import { console } from 'openspan'
|
|
|
4
4
|
import { type bindings, Connector } from '@toa.io/core'
|
|
5
5
|
import * as http from './HTTP'
|
|
6
6
|
import { rethrow } from './exceptions'
|
|
7
|
+
import { decide } from './Branch'
|
|
7
8
|
import type { Interception } from './Interception'
|
|
8
9
|
import type { Method, Node, Parameter, Tree, Match } from './RTD'
|
|
9
10
|
import type { Label } from './discovery'
|
|
10
|
-
import type { Branch } from './Branch'
|
|
11
|
+
import type { Branch, Exposed } from './Branch'
|
|
11
12
|
|
|
12
13
|
export class Gateway extends Connector {
|
|
13
14
|
private readonly broadcast: Broadcast
|
|
@@ -70,11 +71,19 @@ export class Gateway extends Connector {
|
|
|
70
71
|
console.info('Gateway started')
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Both of these reach into components, and a dependency is torn down only once this has
|
|
76
|
+
* returned — where `dispose` runs after every one of them already has. The throttling
|
|
77
|
+
* ticker firing in between calls an endpoint that has just been unbound, and reports the
|
|
78
|
+
* refusal as a failure to reconcile.
|
|
79
|
+
*/
|
|
80
|
+
protected override async close (): Promise<void> {
|
|
74
81
|
this.stopped = true
|
|
75
82
|
|
|
76
83
|
this.tree.dispose()
|
|
84
|
+
}
|
|
77
85
|
|
|
86
|
+
protected override dispose (): void {
|
|
78
87
|
console.info('Gateway is closed')
|
|
79
88
|
}
|
|
80
89
|
|
|
@@ -223,13 +232,24 @@ export class Gateway extends Connector {
|
|
|
223
232
|
|
|
224
233
|
const exposed = this.branches.get(id)
|
|
225
234
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
235
|
+
if (exposed !== undefined)
|
|
236
|
+
switch (decide(exposed, branch)) {
|
|
237
|
+
// rebuilding an identical branch would only tear down its live endpoints
|
|
238
|
+
case 'refresh':
|
|
239
|
+
this.tree.refresh(exposed.nodes)
|
|
240
|
+
console.trace('Branch refreshed', attributes)
|
|
230
241
|
|
|
231
|
-
|
|
232
|
-
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
case 'superseded':
|
|
245
|
+
console.debug('Branch superseded', {
|
|
246
|
+
...attributes,
|
|
247
|
+
timestamp: branch.timestamp,
|
|
248
|
+
exposed: exposed.timestamp
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
return
|
|
252
|
+
}
|
|
233
253
|
|
|
234
254
|
let nodes: Node[]
|
|
235
255
|
|
|
@@ -243,7 +263,7 @@ export class Gateway extends Connector {
|
|
|
243
263
|
return
|
|
244
264
|
}
|
|
245
265
|
|
|
246
|
-
this.branches.set(id, { version: branch.version, nodes })
|
|
266
|
+
this.branches.set(id, { version: branch.version, timestamp: branch.timestamp, nodes })
|
|
247
267
|
|
|
248
268
|
if (this.lastMerge !== 0)
|
|
249
269
|
this.widestGap = Math.max(this.widestGap, Date.now() - this.lastMerge)
|
|
@@ -258,11 +278,6 @@ export class Gateway extends Connector {
|
|
|
258
278
|
|
|
259
279
|
export type Broadcast = bindings.Broadcast<Label>
|
|
260
280
|
|
|
261
|
-
interface Exposed {
|
|
262
|
-
version: string
|
|
263
|
-
nodes: Node[]
|
|
264
|
-
}
|
|
265
|
-
|
|
266
281
|
const SETTLE_QUIET_MIN = 500
|
|
267
282
|
const SETTLE_QUIET_MAX = 10_000
|
|
268
283
|
const SETTLE_QUIET_FACTOR = 2
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { Err } from 'error-value'
|
|
2
1
|
import { encode } from './yaml'
|
|
3
2
|
|
|
4
3
|
it('encodes error values as their public properties', () => {
|
|
5
|
-
const value = new
|
|
4
|
+
const value = new (class InvalidUsernameError extends Error {
|
|
5
|
+
public readonly code = 'INVALID_USERNAME'
|
|
6
|
+
public override readonly message = 'Username is not meeting the requirements'
|
|
7
|
+
})()
|
|
6
8
|
|
|
7
9
|
expect(encode(value).toString())
|
|
8
10
|
.toBe('code: INVALID_USERNAME\nmessage: Username is not meeting the requirements\n')
|
package/source/RTD/Tree.ts
CHANGED
|
@@ -59,7 +59,10 @@ export class Tree {
|
|
|
59
59
|
endpoints: this.endpoints,
|
|
60
60
|
directives: {
|
|
61
61
|
factory: this.directives,
|
|
62
|
-
|
|
62
|
+
// A merged branch is mounted under the root, so it inherits the root's
|
|
63
|
+
// directives. The trunk is the root: createNode adds them itself, and
|
|
64
|
+
// seeding them here too would apply every one of them twice.
|
|
65
|
+
stack: node === this.root ? [] : this.root.directives ?? []
|
|
63
66
|
},
|
|
64
67
|
path: label(extension),
|
|
65
68
|
extension
|
package/source/Tenant.ts
CHANGED
|
@@ -7,11 +7,11 @@ import type { Branch } from './Branch'
|
|
|
7
7
|
|
|
8
8
|
export class Tenant extends Connector {
|
|
9
9
|
private readonly broadcast: Broadcast
|
|
10
|
-
private readonly branch: Branch
|
|
10
|
+
private readonly branch: Omit<Branch, 'timestamp'>
|
|
11
11
|
private started = 0
|
|
12
12
|
private stopped = false
|
|
13
13
|
|
|
14
|
-
public constructor (broadcast: Broadcast, branch: Branch) {
|
|
14
|
+
public constructor (broadcast: Broadcast, branch: Omit<Branch, 'timestamp'>) {
|
|
15
15
|
super()
|
|
16
16
|
|
|
17
17
|
this.broadcast = broadcast
|
|
@@ -29,7 +29,13 @@ export class Tenant extends Connector {
|
|
|
29
29
|
void this.announce()
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Announcing is stopped where the teardown begins, not in `dispose`, which a connector
|
|
34
|
+
* runs after every one of its dependencies has gone. A component on its way out that
|
|
35
|
+
* announces itself once more has its routes held open by whoever is listening, and the
|
|
36
|
+
* requests that follow reach nothing.
|
|
37
|
+
*/
|
|
38
|
+
protected override async close (): Promise<void> {
|
|
33
39
|
this.stopped = true
|
|
34
40
|
}
|
|
35
41
|
|
|
@@ -47,7 +53,12 @@ export class Tenant extends Connector {
|
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
private async expose (): Promise<void> {
|
|
50
|
-
|
|
56
|
+
// the ping subscription outlives the announcing loop, and answering one on the way out
|
|
57
|
+
// is the same announcement by another route
|
|
58
|
+
if (this.stopped)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
await this.broadcast.transmit('expose', { ...this.branch, timestamp: this.started })
|
|
51
62
|
}
|
|
52
63
|
}
|
|
53
64
|
|
package/source/deployment.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
2
|
import { type Dependency, type Service } from '@toa.io/operations'
|
|
3
|
-
import { encode } from '@toa.io/generic'
|
|
4
3
|
import { type Annotation } from './Annotation'
|
|
5
4
|
import * as schemas from './schemas'
|
|
6
5
|
import { shortcuts } from './Directive'
|
|
@@ -35,7 +34,7 @@ export function deployment (_: unknown, annotation?: Annotation): Dependency {
|
|
|
35
34
|
|
|
36
35
|
service.variables!.push({
|
|
37
36
|
name: 'TOA_EXPOSITION',
|
|
38
|
-
value:
|
|
37
|
+
value: JSON.stringify(tree)
|
|
39
38
|
})
|
|
40
39
|
}
|
|
41
40
|
|
|
@@ -57,13 +56,13 @@ export function deployment (_: unknown, annotation?: Annotation): Dependency {
|
|
|
57
56
|
|
|
58
57
|
service.variables!.push({
|
|
59
58
|
name: 'TOA_EXPOSITION_PROPERTIES',
|
|
60
|
-
value:
|
|
59
|
+
value: JSON.stringify(properties)
|
|
61
60
|
})
|
|
62
61
|
|
|
63
62
|
// Nested identity composition shares this process; gateway already exposes /.ready.
|
|
64
63
|
service.variables!.push({
|
|
65
64
|
name: 'TOA_TELEMETRY_READY',
|
|
66
|
-
value:
|
|
65
|
+
value: JSON.stringify(false)
|
|
67
66
|
})
|
|
68
67
|
|
|
69
68
|
return { services: [service] }
|
|
@@ -11,7 +11,7 @@ export class IO implements DirectiveFamily<Directive> {
|
|
|
11
11
|
public readonly name = 'io'
|
|
12
12
|
public readonly mandatory = true
|
|
13
13
|
|
|
14
|
-
/** Throttling reconciles through a component, because only a component has
|
|
14
|
+
/** Throttling reconciles through a component, because only a component has an atom aspect. */
|
|
15
15
|
private sync: Sync | null = null
|
|
16
16
|
|
|
17
17
|
// eslint-disable-next-line max-params
|
|
@@ -25,7 +25,7 @@ export class IO implements DirectiveFamily<Directive> {
|
|
|
25
25
|
|
|
26
26
|
// discovering boots the component, so nothing is discovered until something throttles
|
|
27
27
|
if (name === 'throttle')
|
|
28
|
-
this.sync ??= new Sync(remotes.discover('exposition', '
|
|
28
|
+
this.sync ??= new Sync(remotes.discover('exposition', 'atom'))
|
|
29
29
|
|
|
30
30
|
return new Directive(value, this.sync!, route)
|
|
31
31
|
}
|
|
@@ -11,7 +11,7 @@ import type { Remote } from '@toa.io/core'
|
|
|
11
11
|
* the debt the whole group has reached, which the quotas then decide on locally.
|
|
12
12
|
*/
|
|
13
13
|
export class Sync {
|
|
14
|
-
private readonly
|
|
14
|
+
private readonly atom: Promise<Remote>
|
|
15
15
|
private readonly quotas: Quotas[] = []
|
|
16
16
|
private remote: Remote | null = null
|
|
17
17
|
private timer: NodeJS.Timeout | null = null
|
|
@@ -20,8 +20,8 @@ export class Sync {
|
|
|
20
20
|
/** Ticks do not overlap: a slow round trip delays reconciling, it does not double it. */
|
|
21
21
|
private reconciling = false
|
|
22
22
|
|
|
23
|
-
public constructor (
|
|
24
|
-
this.
|
|
23
|
+
public constructor (atom: Promise<Remote>) {
|
|
24
|
+
this.atom = atom
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
public register (quotas: Quotas): void {
|
|
@@ -74,7 +74,7 @@ export class Sync {
|
|
|
74
74
|
|
|
75
75
|
private async reconcile (batch: Batch[]): Promise<void> {
|
|
76
76
|
try {
|
|
77
|
-
this.remote ??= await this.
|
|
77
|
+
this.remote ??= await this.atom
|
|
78
78
|
|
|
79
79
|
const keys = batch.map((entry) => entry.quotas.name(entry.key))
|
|
80
80
|
const deltas = batch.map((entry) => entry.delta)
|
|
@@ -3,6 +3,13 @@ import { cors } from '../cors'
|
|
|
3
3
|
import { Mapping } from './Mapping'
|
|
4
4
|
import type { Input } from '../../io'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Forbidden request header names: the browser sets them itself and a script cannot
|
|
8
|
+
* list them in `Access-Control-Request-Headers`, so there is nothing to advertise
|
|
9
|
+
* or vary on. The value is still read, it just never reaches CORS.
|
|
10
|
+
*/
|
|
11
|
+
const FORBIDDEN = new Set(['host', 'origin'])
|
|
12
|
+
|
|
6
13
|
export class Headers extends Mapping<Record<string, string>> {
|
|
7
14
|
private readonly headers: string[]
|
|
8
15
|
|
|
@@ -14,7 +21,7 @@ export class Headers extends Mapping<Record<string, string>> {
|
|
|
14
21
|
|
|
15
22
|
super(map)
|
|
16
23
|
|
|
17
|
-
this.headers = Object.values(map).filter((header) => header
|
|
24
|
+
this.headers = Object.values(map).filter((header) => !FORBIDDEN.has(header))
|
|
18
25
|
this.headers.forEach((header) => cors.allow(header))
|
|
19
26
|
}
|
|
20
27
|
|
|
@@ -11,7 +11,6 @@ import type { Parameter } from '../../RTD'
|
|
|
11
11
|
import type { Unit, Location } from './workflows'
|
|
12
12
|
import type { Entry } from '@toa.io/extensions.storages'
|
|
13
13
|
import type { Remotes } from '../../Remotes'
|
|
14
|
-
import type { Err } from 'error-value'
|
|
15
14
|
import type { Component } from '@toa.io/core'
|
|
16
15
|
import type { Output } from '../../io'
|
|
17
16
|
import type { Input } from './types'
|
|
@@ -73,7 +72,7 @@ export class Put extends Directive {
|
|
|
73
72
|
const entry = await this.storage.invoke<Entry>('put', request)
|
|
74
73
|
|
|
75
74
|
return match<Output>(entry,
|
|
76
|
-
Error, (error:
|
|
75
|
+
Error, (error: CodedError) => this.throw(error),
|
|
77
76
|
() => this.reply(input, storage, entry, parameters))
|
|
78
77
|
}
|
|
79
78
|
|
|
@@ -104,7 +103,7 @@ export class Put extends Directive {
|
|
|
104
103
|
return stream
|
|
105
104
|
}
|
|
106
105
|
|
|
107
|
-
private throw (error:
|
|
106
|
+
private throw (error: CodedError): never {
|
|
108
107
|
throw match(error.code,
|
|
109
108
|
'NOT_ACCEPTABLE', () => new http.UnsupportedMediaType(),
|
|
110
109
|
'TYPE_MISMATCH', () => new http.BadRequest(),
|
|
@@ -136,3 +135,7 @@ interface StoreRequest {
|
|
|
136
135
|
attributes?: Record<string, string>
|
|
137
136
|
}
|
|
138
137
|
}
|
|
138
|
+
|
|
139
|
+
interface CodedError extends Error {
|
|
140
|
+
code: string
|
|
141
|
+
}
|
package/source/root.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { merge } from '@toa.io/generic'
|
|
2
2
|
import { syntax } from './RTD'
|
|
3
3
|
|
|
4
4
|
export function resolve (): syntax.Node {
|
|
5
5
|
const value = process.env.TOA_EXPOSITION
|
|
6
|
-
const root = value !== undefined ?
|
|
6
|
+
const root = value !== undefined ? JSON.parse(value) as syntax.Node : syntax.createNode()
|
|
7
7
|
|
|
8
8
|
merge(root, PREDEFINED)
|
|
9
9
|
|
package/transpiled/Branch.d.ts
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import type * as RTD from './RTD/syntax';
|
|
2
|
+
import type { Node } from './RTD';
|
|
2
3
|
export interface Branch {
|
|
3
4
|
namespace: string;
|
|
4
5
|
component: string;
|
|
5
6
|
isolated: boolean;
|
|
6
7
|
node: RTD.Node;
|
|
7
8
|
version: string;
|
|
9
|
+
/**
|
|
10
|
+
* When the tenant that announced it started, by its own clock. A replica on its way
|
|
11
|
+
* out announces the same component with an older stamp than the one replacing it, and
|
|
12
|
+
* that is what tells the two apart when their versions differ.
|
|
13
|
+
*/
|
|
14
|
+
timestamp: number;
|
|
8
15
|
}
|
|
16
|
+
/** A branch the gateway has merged, and the tenant it came from. */
|
|
17
|
+
export interface Exposed {
|
|
18
|
+
version: string;
|
|
19
|
+
/** The start time of the tenant this branch came from. */
|
|
20
|
+
timestamp: number;
|
|
21
|
+
nodes: Node[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* What to do with an announcement about a component that already has a branch exposed.
|
|
25
|
+
*
|
|
26
|
+
* - `refresh`: the same version, so its expiration is extended and its endpoints left alone
|
|
27
|
+
* - `superseded`: it came from a tenant that started before the one exposed now
|
|
28
|
+
* - `merge`: everything else
|
|
29
|
+
*/
|
|
30
|
+
export declare function decide(exposed: Exposed, branch: Branch): Decision;
|
|
31
|
+
export type Decision = 'merge' | 'refresh' | 'superseded';
|
package/transpiled/Branch.js
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decide = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* What to do with an announcement about a component that already has a branch exposed.
|
|
6
|
+
*
|
|
7
|
+
* - `refresh`: the same version, so its expiration is extended and its endpoints left alone
|
|
8
|
+
* - `superseded`: it came from a tenant that started before the one exposed now
|
|
9
|
+
* - `merge`: everything else
|
|
10
|
+
*/
|
|
11
|
+
function decide(exposed, branch) {
|
|
12
|
+
if (exposed.version === branch.version)
|
|
13
|
+
return 'refresh';
|
|
14
|
+
if (branch.timestamp < exposed.timestamp)
|
|
15
|
+
return 'superseded';
|
|
16
|
+
return 'merge';
|
|
17
|
+
}
|
|
18
|
+
exports.decide = decide;
|
|
3
19
|
//# sourceMappingURL=Branch.js.map
|
package/transpiled/Branch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Branch.js","sourceRoot":"","sources":["../source/Branch.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"Branch.js","sourceRoot":"","sources":["../source/Branch.ts"],"names":[],"mappings":";;;AA4BA;;;;;;GAMG;AACH,SAAgB,MAAM,CAAE,OAAgB,EAAE,MAAc;IACtD,IAAI,OAAO,CAAC,OAAO,KAAK,MAAM,CAAC,OAAO;QACpC,OAAO,SAAS,CAAA;IAElB,IAAI,MAAM,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS;QACtC,OAAO,YAAY,CAAA;IAErB,OAAO,OAAO,CAAA;AAChB,CAAC;AARD,wBAQC"}
|
package/transpiled/Factory.js
CHANGED
|
@@ -30,7 +30,6 @@ exports.Factory = void 0;
|
|
|
30
30
|
const node_assert_1 = __importDefault(require("node:assert"));
|
|
31
31
|
const node_crypto_1 = require("node:crypto");
|
|
32
32
|
const openspan_1 = require("openspan");
|
|
33
|
-
const generic_1 = require("@toa.io/generic");
|
|
34
33
|
const Tenant_1 = require("./Tenant");
|
|
35
34
|
const Gateway_1 = require("./Gateway");
|
|
36
35
|
const Remotes_1 = require("./Remotes");
|
|
@@ -50,6 +49,7 @@ class Factory {
|
|
|
50
49
|
tenant(locator, node) {
|
|
51
50
|
const broadcast = this.boot.bindings.broadcast(CHANNEL, locator.id);
|
|
52
51
|
const hash = (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(node)).digest('hex');
|
|
52
|
+
// no timestamp: the tenant stamps each announcement with its own start time
|
|
53
53
|
const branch = {
|
|
54
54
|
namespace: locator.namespace,
|
|
55
55
|
component: locator.name,
|
|
@@ -62,7 +62,7 @@ class Factory {
|
|
|
62
62
|
service() {
|
|
63
63
|
node_assert_1.default.ok(process.env.TOA_EXPOSITION_PROPERTIES, 'TOA_EXPOSITION_PROPERTIES is undefined');
|
|
64
64
|
configureLogs();
|
|
65
|
-
const options =
|
|
65
|
+
const options = JSON.parse(process.env.TOA_EXPOSITION_PROPERTIES);
|
|
66
66
|
const broadcast = this.boot.bindings.broadcast(CHANNEL);
|
|
67
67
|
const server = http.Server.create({ ...options });
|
|
68
68
|
const remotes = new Remotes_1.Remotes(this.boot);
|
|
@@ -87,10 +87,10 @@ const TRACES_ENV = 'TOA_TELEMETRY_TRACES';
|
|
|
87
87
|
function configureLogs() {
|
|
88
88
|
const globEnv = process.env[LOGS_PREFIX];
|
|
89
89
|
const level = process.env.TOA_DEV === '1' ? 'trace' : 'info';
|
|
90
|
-
const options = globEnv === undefined ? { level } :
|
|
90
|
+
const options = globEnv === undefined ? { level } : JSON.parse(globEnv);
|
|
91
91
|
openspan_1.console.configure({ level: options.level ?? level });
|
|
92
92
|
const tracesEnv = process.env[TRACES_ENV];
|
|
93
|
-
(0, openspan_1.traces)(tracesEnv === undefined ? development() :
|
|
93
|
+
(0, openspan_1.traces)(tracesEnv === undefined ? development() : JSON.parse(tracesEnv));
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
96
|
* Tracing is off unless it is configured. The console exporter is a local development
|