@toa.io/extensions.exposition 0.22.0 → 0.23.0-dev.0

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.
Files changed (74) hide show
  1. package/components/identity.basic/source/authenticate.ts +9 -6
  2. package/components/identity.basic/source/create.ts +1 -2
  3. package/components/identity.basic/source/transit.ts +10 -6
  4. package/components/identity.basic/source/types.ts +2 -2
  5. package/components/identity.tokens/source/authenticate.ts +6 -5
  6. package/components/identity.tokens/source/decrypt.ts +12 -4
  7. package/components/identity.tokens/source/encrypt.test.ts +1 -1
  8. package/components/identity.tokens/source/types.ts +2 -2
  9. package/components/octets.storage/manifest.toa.yaml +26 -0
  10. package/components/octets.storage/operations/delete.js +7 -0
  11. package/components/octets.storage/operations/fetch.js +46 -0
  12. package/components/octets.storage/operations/get.js +7 -0
  13. package/components/octets.storage/operations/list.js +7 -0
  14. package/components/octets.storage/operations/permute.js +7 -0
  15. package/components/octets.storage/operations/store.js +11 -0
  16. package/cucumber.js +0 -1
  17. package/documentation/octets.md +196 -0
  18. package/documentation/protocol.md +49 -5
  19. package/features/access.feature +1 -0
  20. package/features/errors.feature +18 -0
  21. package/features/identity.basic.feature +2 -0
  22. package/features/octets.feature +295 -0
  23. package/features/octets.workflows.feature +114 -0
  24. package/features/routes.feature +40 -0
  25. package/features/steps/HTTP.ts +47 -5
  26. package/features/steps/Parameters.ts +5 -1
  27. package/features/steps/Workspace.ts +3 -2
  28. package/features/steps/components/octets.tester/manifest.toa.yaml +15 -0
  29. package/features/steps/components/octets.tester/operations/bar.js +12 -0
  30. package/features/steps/components/octets.tester/operations/baz.js +11 -0
  31. package/features/steps/components/octets.tester/operations/diversify.js +14 -0
  32. package/features/steps/components/octets.tester/operations/err.js +16 -0
  33. package/features/steps/components/octets.tester/operations/foo.js +7 -0
  34. package/features/steps/components/octets.tester/operations/lenna.png +0 -0
  35. package/features/steps/components/pots/manifest.toa.yaml +1 -1
  36. package/features/streams.feature +5 -1
  37. package/package.json +11 -10
  38. package/readme.md +7 -5
  39. package/schemas/octets/context.cos.yaml +1 -0
  40. package/schemas/octets/delete.cos.yaml +1 -0
  41. package/schemas/octets/fetch.cos.yaml +3 -0
  42. package/schemas/octets/list.cos.yaml +1 -0
  43. package/schemas/octets/permute.cos.yaml +1 -0
  44. package/schemas/octets/store.cos.yaml +3 -0
  45. package/source/Gateway.ts +12 -7
  46. package/source/HTTP/Server.fixtures.ts +2 -6
  47. package/source/HTTP/Server.test.ts +7 -31
  48. package/source/HTTP/Server.ts +31 -16
  49. package/source/HTTP/exceptions.ts +2 -12
  50. package/source/HTTP/formats/index.ts +7 -4
  51. package/source/HTTP/formats/json.ts +3 -0
  52. package/source/HTTP/formats/msgpack.ts +3 -0
  53. package/source/HTTP/formats/text.ts +3 -0
  54. package/source/HTTP/formats/yaml.ts +3 -0
  55. package/source/HTTP/messages.test.ts +3 -49
  56. package/source/HTTP/messages.ts +58 -33
  57. package/source/RTD/Route.ts +1 -1
  58. package/source/RTD/segment.ts +2 -1
  59. package/source/Remotes.ts +8 -0
  60. package/source/directives/auth/Family.ts +25 -22
  61. package/source/directives/auth/Incept.ts +3 -3
  62. package/source/directives/auth/Rule.ts +1 -1
  63. package/source/directives/auth/types.ts +2 -2
  64. package/source/directives/index.ts +2 -1
  65. package/source/directives/octets/Context.ts +18 -0
  66. package/source/directives/octets/Delete.ts +32 -0
  67. package/source/directives/octets/Family.ts +68 -0
  68. package/source/directives/octets/Fetch.ts +85 -0
  69. package/source/directives/octets/List.ts +32 -0
  70. package/source/directives/octets/Permute.ts +37 -0
  71. package/source/directives/octets/Store.ts +158 -0
  72. package/source/directives/octets/index.ts +3 -0
  73. package/source/directives/octets/schemas.ts +12 -0
  74. package/source/directives/octets/types.ts +13 -0
@@ -14,6 +14,7 @@ export function fragment (path: string): string[] {
14
14
 
15
15
  function parse (segment: string): Segment {
16
16
  if (segment[0] === ':') return { fragment: null, placeholder: segment.substring(1) }
17
+ else if (segment === '*') return { fragment: null, placeholder: null }
17
18
  else return { fragment: segment }
18
19
  }
19
20
 
@@ -21,5 +22,5 @@ export type Segment = {
21
22
  fragment: string
22
23
  } | {
23
24
  fragment: null
24
- placeholder: string
25
+ placeholder: string | null
25
26
  }
package/source/Remotes.ts CHANGED
@@ -3,6 +3,7 @@ import { type Bootloader } from './Factory'
3
3
 
4
4
  export class Remotes extends Connector {
5
5
  private readonly boot: Bootloader
6
+ private readonly remotes: Record<string, Promise<Component>> = {}
6
7
 
7
8
  public constructor (boot: Bootloader) {
8
9
  super()
@@ -11,6 +12,13 @@ export class Remotes extends Connector {
11
12
 
12
13
  public async discover (namespace: string, name: string): Promise<Component> {
13
14
  const locator = new Locator(name, namespace)
15
+
16
+ this.remotes[locator.id] ??= this.create(locator)
17
+
18
+ return await this.remotes[locator.id]
19
+ }
20
+
21
+ private async create (locator: Locator): Promise<Component> {
14
22
  const remote = await this.boot.remote(locator)
15
23
 
16
24
  this.depends(remote)
@@ -1,32 +1,34 @@
1
- import { type Component } from '@toa.io/core'
2
- import { Nope } from 'nopeable'
3
- import { type Parameter } from '../../RTD'
4
- import { type Family, type Output } from '../../Directive'
5
- import { type Remotes } from '../../Remotes'
1
+ import { match } from 'matchacho'
6
2
  import * as http from '../../HTTP'
7
- import {
8
- type AuthenticationResult,
9
- type Ban,
10
- type Directive,
11
- type Discovery,
12
- type Extension,
13
- type Identity,
14
- type Input, type Remote,
15
- type Schemes
16
- } from './types'
17
3
  import { Anonymous } from './Anonymous'
18
4
  import { Id } from './Id'
19
5
  import { Role } from './Role'
20
6
  import { Rule } from './Rule'
21
7
  import { Incept } from './Incept'
8
+ import { Echo } from './Echo'
22
9
  import { split } from './split'
23
- import { PRIMARY, PROVIDERS } from './schemes'
24
10
  import { Scheme } from './Scheme'
25
- import { Echo } from './Echo'
11
+ import { PRIMARY, PROVIDERS } from './schemes'
12
+ import type { Component } from '@toa.io/core'
13
+ import type { Remotes } from '../../Remotes'
14
+ import type { Family, Output } from '../../Directive'
15
+ import type { Parameter } from '../../RTD'
16
+ import type {
17
+ AuthenticationResult,
18
+ Ban,
19
+ Directive,
20
+ Discovery,
21
+ Extension,
22
+ Identity,
23
+ Input,
24
+ Remote,
25
+ Schemes
26
+ } from './types'
26
27
 
27
28
  class Authorization implements Family<Directive, Extension> {
28
29
  public readonly name: string = 'auth'
29
30
  public readonly mandatory: boolean = true
31
+
30
32
  private readonly schemes = {} as unknown as Schemes
31
33
  private readonly discovery = {} as unknown as Discovery
32
34
  private tokens: Component | null = null
@@ -41,10 +43,11 @@ class Authorization implements Family<Directive, Extension> {
41
43
  for (const name of REMOTES)
42
44
  this.discovery[name] ??= remotes.discover('identity', name)
43
45
 
44
- if (Class === Role) return new Class(value, this.discovery.roles)
45
- else if (Class === Rule) return new Class(value, this.create.bind(this))
46
- else if (Class === Incept) return new Class(value, this.discovery)
47
- else return new Class(value)
46
+ return match(Class,
47
+ Role, () => new Role(value, this.discovery.roles),
48
+ Rule, () => new Rule(value, this.create.bind(this)),
49
+ Incept, () => new Incept(value, this.discovery),
50
+ () => new Class(value))
48
51
  }
49
52
 
50
53
  public async preflight
@@ -107,7 +110,7 @@ class Authorization implements Family<Directive, Extension> {
107
110
  const result = await this.schemes[scheme]
108
111
  .invoke<AuthenticationResult>('authenticate', { input: credentials })
109
112
 
110
- if (result instanceof Nope)
113
+ if (result instanceof Error)
111
114
  return null
112
115
 
113
116
  const identity = result.identity
@@ -1,4 +1,4 @@
1
- import { Nope, type Nopeable } from 'nopeable'
1
+ import { type Maybe } from '@toa.io/types'
2
2
  import * as http from '../../HTTP'
3
3
  import { type Directive, type Discovery, type Identity, type Input, type Schemes } from './types'
4
4
  import { split } from './split'
@@ -31,9 +31,9 @@ export class Incept implements Directive {
31
31
  this.schemes[scheme] ??= await this.discovery[provider]
32
32
 
33
33
  const identity = await this.schemes[scheme]
34
- .invoke<Nopeable<Identity>>('create', { input: { id, credentials } })
34
+ .invoke<Maybe<Identity>>('create', { input: { id, credentials } })
35
35
 
36
- if (identity instanceof Nope)
36
+ if (identity instanceof Error)
37
37
  throw new http.Conflict(identity)
38
38
 
39
39
  request.identity = identity
@@ -25,4 +25,4 @@ export class Rule implements Directive {
25
25
  }
26
26
  }
27
27
 
28
- type Create = (name: string, value: any) => Directive
28
+ type Create = (name: string, value: any, ...args: any[]) => Directive
@@ -1,5 +1,5 @@
1
1
  import { type Component } from '@toa.io/core'
2
- import { type Nopeable } from 'nopeable'
2
+ import { type Maybe } from '@toa.io/types'
3
3
  import { type Parameter } from '../../RTD'
4
4
  import type * as http from '../../HTTP'
5
5
  import type * as directive from '../../Directive'
@@ -29,7 +29,7 @@ export interface Ban {
29
29
  }
30
30
 
31
31
  export type Input = directive.Input & Extension
32
- export type AuthenticationResult = Nopeable<{ identity: Identity, refresh: boolean }>
32
+ export type AuthenticationResult = Maybe<{ identity: Identity, refresh: boolean }>
33
33
 
34
34
  export type Scheme = 'basic' | 'token'
35
35
  export type Remote = 'basic' | 'tokens' | 'roles' | 'bans'
@@ -1,5 +1,6 @@
1
1
  import { type Family } from '../Directive'
2
2
  import Dev from './dev'
3
3
  import Auth from './auth'
4
+ import Octets from './octets'
4
5
 
5
- export const families: Family[] = [Dev, Auth]
6
+ export const families: Family[] = [Auth, Octets, Dev]
@@ -0,0 +1,18 @@
1
+ import * as schemas from './schemas'
2
+ import type { Output } from '../../Directive'
3
+ import type { Directive } from './types'
4
+
5
+ export class Context implements Directive {
6
+ public readonly targeted = false
7
+ public readonly storage: string
8
+
9
+ public constructor (value: any) {
10
+ schemas.context.validate(value)
11
+
12
+ this.storage = value
13
+ }
14
+
15
+ public apply (): Output {
16
+ return null
17
+ }
18
+ }
@@ -0,0 +1,32 @@
1
+ import { NotFound } from '../../HTTP'
2
+ import * as schemas from './schemas'
3
+ import type { Maybe } from '@toa.io/types'
4
+ import type { Component } from '@toa.io/core'
5
+ import type { Output } from '../../Directive'
6
+
7
+ import type { Directive, Input } from './types'
8
+
9
+ export class Delete implements Directive {
10
+ public readonly targeted = true
11
+
12
+ private readonly discovery: Promise<Component>
13
+ private storage: Component | null = null
14
+
15
+ public constructor (value: null, discovery: Promise<Component>) {
16
+ schemas.remove.validate(value)
17
+
18
+ this.discovery = discovery
19
+ }
20
+
21
+ public async apply (storage: string, request: Input): Promise<Output> {
22
+ this.storage ??= await this.discovery
23
+
24
+ const input = { storage, path: request.path }
25
+ const error = await this.storage.invoke<Maybe<unknown>>('delete', { input })
26
+
27
+ if (error instanceof Error)
28
+ throw new NotFound()
29
+
30
+ return {}
31
+ }
32
+ }
@@ -0,0 +1,68 @@
1
+ import { NotFound } from '../../HTTP'
2
+ import { Context } from './Context'
3
+ import { Store } from './Store'
4
+ import { Fetch } from './Fetch'
5
+ import { List } from './List'
6
+ import { Delete } from './Delete'
7
+ import { Permute } from './Permute'
8
+ import type { Component } from '@toa.io/core'
9
+ import type { Remotes } from '../../Remotes'
10
+ import type { Output, Family } from '../../Directive'
11
+ import type { Directive, Input } from './types'
12
+
13
+ class Octets implements Family<Directive> {
14
+ public readonly name: string = 'octets'
15
+ public readonly mandatory: boolean = false
16
+
17
+ private discovery = null as unknown as Promise<Component>
18
+
19
+ public create (name: string, value: any, remotes: Remotes): Directive {
20
+ const Class = DIRECTIVES[name]
21
+
22
+ if (Class === undefined)
23
+ throw new Error(`Directive '${name}' is not provided by the '${this.name}' family.`)
24
+
25
+ this.discovery ??= remotes.discover('octets', 'storage')
26
+
27
+ return new Class(value, this.discovery, remotes)
28
+ }
29
+
30
+ public async preflight (directives: Directive[], input: Input): Promise<Output> {
31
+ let context: Context | null = null
32
+ let action: Directive | null = null
33
+
34
+ for (const directive of directives)
35
+ if (directive instanceof Context)
36
+ context ??= directive
37
+ else if (action === null)
38
+ action = directive
39
+ else
40
+ throw new Error('Octets action is umbiguous.')
41
+
42
+ if (action === null)
43
+ return null
44
+
45
+ if (context === null)
46
+ throw new Error('Octets context is not defined.')
47
+
48
+ const targeted = input.path[input.path.length - 1] !== '/'
49
+
50
+ if (targeted !== action.targeted)
51
+ throw new NotFound(`Trailing slash is ${action.targeted ? 'redundant' : 'required'}.`)
52
+
53
+ return await action.apply(context.storage, input)
54
+ }
55
+ }
56
+
57
+ const DIRECTIVES: Record<string, Constructor> = {
58
+ context: Context,
59
+ store: Store,
60
+ fetch: Fetch,
61
+ list: List,
62
+ delete: Delete,
63
+ permute: Permute
64
+ }
65
+
66
+ type Constructor = new (value: any, discovery: Promise<Component>, remotes: Remotes) => Directive
67
+
68
+ export = new Octets()
@@ -0,0 +1,85 @@
1
+ import { posix } from 'node:path'
2
+ import { Forbidden, NotFound } from '../../HTTP'
3
+ import * as schemas from './schemas'
4
+ import type { Maybe } from '@toa.io/types'
5
+ import type { Entry } from '@toa.io/extensions.storages'
6
+ import type { Readable } from 'node:stream'
7
+ import type { Component } from '@toa.io/core'
8
+ import type { Output } from '../../Directive'
9
+
10
+ import type { Directive, Input } from './types'
11
+
12
+ export class Fetch implements Directive {
13
+ public readonly targeted = true
14
+
15
+ private readonly permissions: Permissions = { blob: true, meta: false }
16
+ private readonly discovery: Promise<Component>
17
+ private storage: Component = null as unknown as Component
18
+
19
+ public constructor (permissions: Partial<Permissions> | null, discovery: Promise<Component>) {
20
+ schemas.fetch.validate(permissions)
21
+
22
+ Object.assign(this.permissions, permissions)
23
+ this.discovery = discovery
24
+ }
25
+
26
+ public async apply (storage: string, request: Input): Promise<Output> {
27
+ this.storage ??= await this.discovery
28
+
29
+ if (request.url.slice(-5) === ':meta')
30
+ return await this.get(storage, request)
31
+ else
32
+ return await this.fetch(storage, request)
33
+ }
34
+
35
+ private async fetch (storage: string, request: Input): Promise<Output> {
36
+ const filename = posix.basename(request.url)
37
+ const variant = filename.includes('.')
38
+
39
+ if (!variant && !this.permissions.blob)
40
+ throw new Forbidden('BLOB variant must be specified.')
41
+
42
+ if ('if-none-match' in request.headers)
43
+ return { status: 304 }
44
+
45
+ const input = { storage, path: request.url }
46
+ const result = await this.storage.invoke<Maybe<FetchResult>>('fetch', { input })
47
+
48
+ if (result instanceof Error)
49
+ throw new NotFound()
50
+
51
+ const headers = {
52
+ 'content-type': result.type,
53
+ 'content-length': result.size,
54
+ etag: result.checksum
55
+ }
56
+
57
+ return { headers, body: result.stream }
58
+ }
59
+
60
+ private async get (storage: string, request: Input): Promise<Output> {
61
+ if (!this.permissions.meta)
62
+ throw new Forbidden('Metadata is not accessible.')
63
+
64
+ const path = request.url.slice(0, -5)
65
+ const input = { storage, path }
66
+ const entry = await this.storage.invoke<Maybe<Entry>>('get', { input })
67
+
68
+ if (entry instanceof Error)
69
+ throw new NotFound()
70
+
71
+ return { body: entry }
72
+ }
73
+ }
74
+
75
+ interface Permissions {
76
+ blob: boolean
77
+ meta: boolean
78
+ }
79
+
80
+ interface FetchResult {
81
+ stream: Readable
82
+ checksum: string
83
+ size: number
84
+ type: string
85
+ }
@@ -0,0 +1,32 @@
1
+ import { NotFound } from '../../HTTP'
2
+ import * as schemas from './schemas'
3
+ import type { Maybe } from '@toa.io/types'
4
+ import type { Component } from '@toa.io/core'
5
+ import type { Output } from '../../Directive'
6
+
7
+ import type { Directive, Input } from './types'
8
+
9
+ export class List implements Directive {
10
+ public readonly targeted = false
11
+
12
+ private readonly discovery: Promise<Component>
13
+ private storage: Component | null = null
14
+
15
+ public constructor (value: null, discovery: Promise<Component>) {
16
+ schemas.remove.validate(value)
17
+
18
+ this.discovery = discovery
19
+ }
20
+
21
+ public async apply (storage: string, request: Input): Promise<Output> {
22
+ this.storage ??= await this.discovery
23
+
24
+ const input = { storage, path: request.path }
25
+ const list = await this.storage.invoke<Maybe<unknown>>('list', { input })
26
+
27
+ if (list instanceof Error)
28
+ throw new NotFound()
29
+
30
+ return { body: list }
31
+ }
32
+ }
@@ -0,0 +1,37 @@
1
+ import { NotAcceptable, NotFound } from '../../HTTP'
2
+ import * as schemas from './schemas'
3
+ import type { Maybe } from '@toa.io/types'
4
+ import type { Component } from '@toa.io/core'
5
+ import type { Output } from '../../Directive'
6
+
7
+ import type { Directive, Input } from './types'
8
+
9
+ export class Permute implements Directive {
10
+ public readonly targeted = false
11
+
12
+ private readonly discovery: Promise<Component>
13
+ private storage: Component | null = null
14
+
15
+ public constructor (value: null, discovery: Promise<Component>) {
16
+ schemas.remove.validate(value)
17
+
18
+ this.discovery = discovery
19
+ }
20
+
21
+ public async apply (storage: string, request: Input): Promise<Output> {
22
+ this.storage ??= await this.discovery
23
+
24
+ if (request.encoder === null)
25
+ throw new NotAcceptable()
26
+
27
+ const path = request.path
28
+ const list = await request.parse()
29
+ const input = { storage, path, list }
30
+ const error = await this.storage.invoke<Maybe<unknown>>('permute', { input })
31
+
32
+ if (error instanceof Error)
33
+ throw new NotFound()
34
+
35
+ return {}
36
+ }
37
+ }
@@ -0,0 +1,158 @@
1
+ import { Readable } from 'node:stream'
2
+ import { posix } from 'node:path'
3
+ import { match } from 'matchacho'
4
+ import { promex } from '@toa.io/generic'
5
+ import { BadRequest, UnsupportedMediaType } from '../../HTTP'
6
+ import * as schemas from './schemas'
7
+ import type { Maybe } from '@toa.io/types'
8
+ import type { Entry } from '@toa.io/extensions.storages'
9
+ import type { Remotes } from '../../Remotes'
10
+ import type { ErrorType } from 'error-value'
11
+ import type { Component } from '@toa.io/core'
12
+ import type { Output } from '../../Directive'
13
+ import type { Directive, Input } from './types'
14
+
15
+ export class Store implements Directive {
16
+ public readonly targeted = false
17
+
18
+ private readonly accept: string | undefined
19
+ private readonly workflow: Workflow | undefined
20
+ private readonly discovery: Record<string, Promise<Component>> = {}
21
+ private readonly remotes: Remotes
22
+ private readonly components: Record<string, Component> = {}
23
+ private storage: Component | null = null
24
+
25
+ public constructor
26
+ (options: Options | null, discovery: Promise<Component>, remotes: Remotes) {
27
+ schemas.store.validate(options)
28
+
29
+ this.accept = match(options?.accept,
30
+ String, (value: string) => value,
31
+ Array, (types: string[]) => types.join(','),
32
+ undefined)
33
+
34
+ this.workflow = match(options?.workflow,
35
+ Array, (units: Unit[]) => units,
36
+ Object, (unit: Unit) => [unit],
37
+ undefined)
38
+
39
+ this.discovery.storage = discovery
40
+ this.remotes = remotes
41
+ }
42
+
43
+ public async apply (storage: string, request: Input): Promise<Output> {
44
+ this.storage ??= await this.discovery.storage
45
+
46
+ const input = { storage, request, accept: this.accept }
47
+ const entry = await this.storage.invoke('store', { input })
48
+
49
+ return match<Output>(entry,
50
+ Error, (error: ErrorType) => this.throw(error),
51
+ () => this.reply(request, storage, entry))
52
+ }
53
+
54
+ private reply (request: Input, storage: string, entry: Entry): Output {
55
+ const body = this.workflow === undefined
56
+ ? entry
57
+ : Readable.from(this.execute(request, storage, entry))
58
+
59
+ return { body }
60
+ }
61
+
62
+ private throw (error: ErrorType): never {
63
+ throw match(error.code,
64
+ 'NOT_ACCEPTABLE', () => new UnsupportedMediaType(),
65
+ 'TYPE_MISMATCH', () => new BadRequest(),
66
+ error)
67
+ }
68
+
69
+ /* eslint-disable no-useless-return, max-depth */
70
+
71
+ /**
72
+ * Execute workflow units sequentially, steps within a unit in parallel.
73
+ * Yield results as soon as they come.
74
+ *
75
+ * If you need to change this, it may take a while.
76
+ */
77
+ private async * execute (request: Input, storage: string, entry: Entry): AsyncGenerator {
78
+ yield entry
79
+
80
+ const path = posix.join(request.path, entry.id)
81
+ let interrupted = false
82
+
83
+ for (const unit of this.workflow as Workflow) {
84
+ if (interrupted)
85
+ break
86
+
87
+ const steps = Object.keys(unit)
88
+
89
+ // unit result promises queue
90
+ const results = Array.from(steps, promex<unknown>)
91
+ let next = 0
92
+
93
+ // execute steps in parallel
94
+ for (const step of steps)
95
+ // these promises are indirectly awaited in the yield loop
96
+ void (async () => {
97
+ const endpoint = unit[step]
98
+ const context: Context = { storage, path, entry }
99
+ const result = await this.call(endpoint, context)
100
+
101
+ if (interrupted)
102
+ return
103
+
104
+ // as a result is received, resolve the next promise from the queue
105
+ const promise = results[next++]
106
+
107
+ if (result instanceof Error) {
108
+ interrupted = true
109
+ promise.resolve({ error: { step, ...result } })
110
+
111
+ // cancel pending promises
112
+ results[next].resolve(null)
113
+ } else
114
+ promise.resolve({ [step]: result ?? null })
115
+ })().catch((e) => results[next].reject(e))
116
+
117
+ // yield results from the queue as they come
118
+ for (const promise of results) {
119
+ const result = await promise
120
+
121
+ if (result === null) // canceled promise
122
+ break
123
+ else
124
+ yield result
125
+ }
126
+ }
127
+ }
128
+
129
+ private async call (endpoint: string, context: Context): Promise<Maybe<unknown>> {
130
+ const [operation, component, namespace = 'default'] = endpoint.split('.').reverse()
131
+ const key = `${namespace}.${component}`
132
+
133
+ this.components[key] ??= await this.discover(key, namespace, component)
134
+
135
+ return await this.components[key].invoke(operation, { input: context })
136
+ }
137
+
138
+ private async discover (key: string, namespace: string, component: string): Promise<Component> {
139
+ if (this.discovery[key] === undefined)
140
+ this.discovery[key] = this.remotes.discover(namespace, component)
141
+
142
+ return await this.discovery[key]
143
+ }
144
+ }
145
+
146
+ type Unit = Record<string, string>
147
+ type Workflow = Unit[]
148
+
149
+ interface Options {
150
+ accept: string | string[]
151
+ workflow: Workflow | Unit
152
+ }
153
+
154
+ interface Context {
155
+ storage: string
156
+ path: string
157
+ entry: Entry
158
+ }
@@ -0,0 +1,3 @@
1
+ import Family from './Family'
2
+
3
+ export = Family
@@ -0,0 +1,12 @@
1
+ import { resolve } from 'node:path'
2
+ import schemas from '@toa.io/schemas'
3
+
4
+ const path = resolve(__dirname, '../../../schemas/octets')
5
+ const namespace = schemas.namespace(path)
6
+
7
+ export const context = namespace.schema('context')
8
+ export const store = namespace.schema('store')
9
+ export const fetch = namespace.schema('fetch')
10
+ export const remove = namespace.schema('delete')
11
+ export const list = namespace.schema('list')
12
+ export const permute = namespace.schema('permute')
@@ -0,0 +1,13 @@
1
+ import type * as directive from '../../Directive'
2
+
3
+ export interface Directive {
4
+ readonly targeted: boolean
5
+
6
+ apply: (storage: string, input: Input) => directive.Output | Promise<directive.Output>
7
+ }
8
+
9
+ export interface Extension {
10
+ octets?: string
11
+ }
12
+
13
+ export type Input = directive.Input & Extension