@toa.io/boot 1.0.0-alpha.31 → 1.0.0-alpha.310
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 +217 -0
- package/package.json +11 -8
- package/src/atom.js +41 -0
- package/src/atomicity.js +11 -0
- package/src/bindings/broadcast.js +3 -6
- package/src/bindings/constants.js +1 -3
- package/src/bindings/consume.js +8 -8
- package/src/bindings/emit.js +3 -6
- package/src/bindings/factory.js +5 -9
- package/src/bindings/inbound.js +11 -0
- package/src/bindings/index.js +7 -13
- package/src/bindings/outbound.js +10 -0
- package/src/bindings/produce.js +40 -18
- package/src/bindings/receive.js +3 -6
- package/src/bridge.js +22 -16
- package/src/call.js +22 -9
- package/src/cascade.js +16 -10
- package/src/component.js +90 -32
- package/src/composition.js +102 -15
- package/src/context.js +7 -8
- package/src/contract.js +11 -12
- package/src/emission.js +21 -14
- package/src/env.js +19 -12
- package/src/events.js +27 -0
- package/src/extensions/aspects.js +4 -8
- package/src/extensions/component.js +4 -8
- package/src/extensions/context.js +4 -8
- package/src/extensions/destinations.js +27 -0
- package/src/extensions/emitter.js +7 -9
- package/src/extensions/index.js +10 -19
- package/src/extensions/instances.js +2 -4
- package/src/extensions/load.js +7 -7
- package/src/extensions/manage.js +15 -0
- package/src/extensions/receiver.js +5 -9
- package/src/extensions/resolve.js +36 -11
- package/src/extensions/storage.js +6 -9
- package/src/extensions/tenants.js +5 -9
- package/src/guards.js +20 -0
- package/src/host.js +29 -0
- package/src/inbox.js +10 -0
- package/src/index.js +27 -34
- package/src/manifest.js +78 -36
- package/src/manifest.test.js +85 -8
- package/src/map.js +98 -0
- package/src/operation.js +25 -13
- package/src/outbox.js +22 -0
- package/src/rc.js +5 -0
- package/src/receivers.js +53 -23
- package/src/remote.js +37 -22
- package/src/span.js +19 -0
- package/src/storage.js +50 -16
- package/src/storage.test.js +57 -0
- package/src/workload.js +281 -0
- package/src/workload.test.js +321 -0
- package/types/bindings.d.ts +6 -2
- package/types/index.d.ts +62 -10
- package/types/manifest.d.ts +5 -4
- package/src/discovery.js +0 -43
- package/src/exposition.js +0 -17
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { describe, it, before, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { Connector } from '@toa.io/core'
|
|
4
|
+
import { environment, timeout } from '@toa.io/generic'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* The probe binds a port and the halt listener reaches a broker, and this suite is about
|
|
8
|
+
* neither: what it exercises is the workload's own lifecycle, so the extensions that would
|
|
9
|
+
* contribute a resident are off. `TOA_INTROSPECTION` is unset, so introspection's is `null`.
|
|
10
|
+
*/
|
|
11
|
+
environment.set('TOA_TELEMETRY_READY', 'false')
|
|
12
|
+
|
|
13
|
+
let Workload
|
|
14
|
+
|
|
15
|
+
before(async () => {
|
|
16
|
+
;({ Workload } = await import('./workload.js'))
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
let sequence
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
sequence = []
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
class TestConnector extends Connector {
|
|
26
|
+
#label
|
|
27
|
+
|
|
28
|
+
constructor(label) {
|
|
29
|
+
super()
|
|
30
|
+
this.#label = label
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async open() {
|
|
34
|
+
sequence.push(`+${this.#label}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async close() {
|
|
38
|
+
sequence.push(`-${this.#label}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async dispose() {
|
|
42
|
+
sequence.push(`*${this.#label}`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One that has something to stop doing, which most connectors have not. */
|
|
47
|
+
class QuietConnector extends Connector {
|
|
48
|
+
#label
|
|
49
|
+
|
|
50
|
+
constructor(label) {
|
|
51
|
+
super()
|
|
52
|
+
this.#label = label
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async pause() {
|
|
56
|
+
sequence.push(`~${this.#label}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async unpause() {
|
|
60
|
+
sequence.push(`^${this.#label}`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Waits for a condition the halt reaches on its own timers. */
|
|
65
|
+
async function until(condition, limit = 2000) {
|
|
66
|
+
const deadline = Date.now() + limit
|
|
67
|
+
|
|
68
|
+
while (Date.now() < deadline) {
|
|
69
|
+
if (condition()) return
|
|
70
|
+
|
|
71
|
+
await timeout(5)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
assert.fail('condition was not reached')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('Workload', () => {
|
|
78
|
+
it('should connect what the command built', async () => {
|
|
79
|
+
const workload = new Workload(async () => new TestConnector('a'))
|
|
80
|
+
|
|
81
|
+
await workload.connect()
|
|
82
|
+
|
|
83
|
+
assert.deepStrictEqual(sequence, ['+a'])
|
|
84
|
+
|
|
85
|
+
await workload.disconnect()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should take down what a gate holds, and leave the rest connected', async () => {
|
|
89
|
+
let built = 0
|
|
90
|
+
|
|
91
|
+
const workload = new Workload(async (workload) => {
|
|
92
|
+
const gate = workload.gate(async () => {
|
|
93
|
+
built++
|
|
94
|
+
|
|
95
|
+
return new TestConnector('gated')
|
|
96
|
+
})
|
|
97
|
+
const root = new Connector()
|
|
98
|
+
|
|
99
|
+
root.depends([gate, new TestConnector('above')])
|
|
100
|
+
|
|
101
|
+
return root
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
await workload.connect()
|
|
105
|
+
|
|
106
|
+
assert.deepStrictEqual(sequence.sort(), ['+above', '+gated'])
|
|
107
|
+
|
|
108
|
+
sequence = []
|
|
109
|
+
|
|
110
|
+
// a hundredth of a second, so the window is over before the assertions are
|
|
111
|
+
workload.stop(0.01)
|
|
112
|
+
|
|
113
|
+
// `-gated` and not `-above`: what a gate holds is closed, and nothing else is
|
|
114
|
+
await until(() => sequence.includes('*gated'))
|
|
115
|
+
|
|
116
|
+
assert.deepStrictEqual(sequence, ['-gated', '*gated'])
|
|
117
|
+
|
|
118
|
+
await until(() => built === 2)
|
|
119
|
+
|
|
120
|
+
assert.deepStrictEqual(sequence, ['-gated', '*gated', '+gated'])
|
|
121
|
+
|
|
122
|
+
await workload.disconnect()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should build again rather than reconnect', async () => {
|
|
126
|
+
const held = []
|
|
127
|
+
|
|
128
|
+
const workload = new Workload(async (workload) =>
|
|
129
|
+
workload.gate(async () => {
|
|
130
|
+
const connector = new TestConnector('a')
|
|
131
|
+
|
|
132
|
+
held.push(connector)
|
|
133
|
+
|
|
134
|
+
return connector
|
|
135
|
+
})
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
await workload.connect()
|
|
139
|
+
|
|
140
|
+
workload.stop(0.01)
|
|
141
|
+
|
|
142
|
+
await until(() => held.length === 2)
|
|
143
|
+
|
|
144
|
+
assert.notEqual(held[0], held[1])
|
|
145
|
+
|
|
146
|
+
await workload.disconnect()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should return before anything is closed', async () => {
|
|
150
|
+
const workload = new Workload(async (workload) =>
|
|
151
|
+
workload.gate(async () => new TestConnector('a'))
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
await workload.connect()
|
|
155
|
+
|
|
156
|
+
sequence = []
|
|
157
|
+
workload.stop(0.01)
|
|
158
|
+
|
|
159
|
+
// the caller is a consumer callback: the teardown must not have begun under it
|
|
160
|
+
assert.deepStrictEqual(sequence, [])
|
|
161
|
+
|
|
162
|
+
await until(() => sequence.includes('+a'))
|
|
163
|
+
await workload.disconnect()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should make one cycle of two overlapping halts', async () => {
|
|
167
|
+
let built = 0
|
|
168
|
+
|
|
169
|
+
const workload = new Workload(async (workload) =>
|
|
170
|
+
workload.gate(async () => {
|
|
171
|
+
built++
|
|
172
|
+
|
|
173
|
+
return new TestConnector('a')
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
await workload.connect()
|
|
178
|
+
|
|
179
|
+
workload.stop(0.01)
|
|
180
|
+
workload.stop(0.01)
|
|
181
|
+
|
|
182
|
+
await until(() => built === 2)
|
|
183
|
+
await timeout(50)
|
|
184
|
+
|
|
185
|
+
assert.equal(built, 2)
|
|
186
|
+
|
|
187
|
+
await workload.disconnect()
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should not halt what is not connected', async () => {
|
|
191
|
+
const workload = new Workload(async () => new TestConnector('a'))
|
|
192
|
+
|
|
193
|
+
workload.stop(0.01)
|
|
194
|
+
|
|
195
|
+
await timeout(30)
|
|
196
|
+
|
|
197
|
+
assert.deepStrictEqual(sequence, [])
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('should quiesce what it holds, closing nothing', async () => {
|
|
201
|
+
const workload = new Workload(async (workload) =>
|
|
202
|
+
workload.gate(async () => new QuietConnector('a'))
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
await workload.connect()
|
|
206
|
+
|
|
207
|
+
sequence = []
|
|
208
|
+
|
|
209
|
+
await workload.quiesce()
|
|
210
|
+
|
|
211
|
+
assert.deepStrictEqual(sequence, ['~a'])
|
|
212
|
+
assert.equal(workload.quiescent(), true)
|
|
213
|
+
|
|
214
|
+
// nothing was taken down, so the process is still working as far as anyone watching it
|
|
215
|
+
assert.equal(workload.running(), true)
|
|
216
|
+
|
|
217
|
+
await workload.disconnect()
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('should undo a quiesce without building anything', async () => {
|
|
221
|
+
let built = 0
|
|
222
|
+
|
|
223
|
+
const workload = new Workload(async (workload) =>
|
|
224
|
+
workload.gate(async () => {
|
|
225
|
+
built++
|
|
226
|
+
|
|
227
|
+
return new QuietConnector('a')
|
|
228
|
+
})
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
await workload.connect()
|
|
232
|
+
await workload.quiesce()
|
|
233
|
+
|
|
234
|
+
sequence = []
|
|
235
|
+
|
|
236
|
+
await workload.cancel()
|
|
237
|
+
|
|
238
|
+
assert.deepStrictEqual(sequence, ['^a'])
|
|
239
|
+
assert.equal(workload.quiescent(), false)
|
|
240
|
+
assert.equal(built, 1)
|
|
241
|
+
|
|
242
|
+
await workload.disconnect()
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('should take down a process that is already quiet', async () => {
|
|
246
|
+
const workload = new Workload(async (workload) =>
|
|
247
|
+
workload.gate(async () => {
|
|
248
|
+
const root = new Connector()
|
|
249
|
+
|
|
250
|
+
root.depends([new QuietConnector('q'), new TestConnector('a')])
|
|
251
|
+
|
|
252
|
+
return root
|
|
253
|
+
})
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
await workload.connect()
|
|
257
|
+
await workload.quiesce()
|
|
258
|
+
|
|
259
|
+
sequence = []
|
|
260
|
+
workload.stop(0.01)
|
|
261
|
+
|
|
262
|
+
await until(() => sequence.includes('+a'))
|
|
263
|
+
|
|
264
|
+
// quieted once, on the quiesce, and not again on the way down
|
|
265
|
+
assert.deepStrictEqual(sequence, ['-a', '*a', '+a'])
|
|
266
|
+
|
|
267
|
+
await workload.disconnect()
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should not let a cancel undo a stop', async () => {
|
|
271
|
+
const workload = new Workload(async (workload) =>
|
|
272
|
+
workload.gate(async () => new TestConnector('a'))
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
await workload.connect()
|
|
276
|
+
|
|
277
|
+
sequence = []
|
|
278
|
+
workload.stop(0.01)
|
|
279
|
+
|
|
280
|
+
await workload.cancel()
|
|
281
|
+
|
|
282
|
+
// it goes down and comes back: a cancel is for a quiesce, and this is past one
|
|
283
|
+
await until(() => sequence.includes('+a'))
|
|
284
|
+
|
|
285
|
+
assert.deepStrictEqual(sequence, ['-a', '*a', '+a'])
|
|
286
|
+
|
|
287
|
+
await workload.disconnect()
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('should keep the process alive while it is halted', async () => {
|
|
291
|
+
const workload = new Workload(async (workload) =>
|
|
292
|
+
workload.gate(async () => new TestConnector('a'))
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
await workload.connect()
|
|
296
|
+
|
|
297
|
+
// long enough that the process would have to be held open for it
|
|
298
|
+
workload.stop(10)
|
|
299
|
+
|
|
300
|
+
await until(() => sequence.includes('*a'))
|
|
301
|
+
|
|
302
|
+
// a halted process holds nothing else: unreferenced, this timer would let it exit
|
|
303
|
+
assert.ok(
|
|
304
|
+
process.getActiveResourcesInfo().includes('Timeout'),
|
|
305
|
+
'nothing keeps the process alive until the rebuild'
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
await workload.disconnect()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('should take the gates down on its own disconnection', async () => {
|
|
312
|
+
const workload = new Workload(async (workload) =>
|
|
313
|
+
workload.gate(async () => new TestConnector('a'))
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
await workload.connect()
|
|
317
|
+
await workload.disconnect()
|
|
318
|
+
|
|
319
|
+
assert.deepStrictEqual(sequence, ['+a', '-a', '*a'])
|
|
320
|
+
})
|
|
321
|
+
})
|
package/types/bindings.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
import type { bindings } from '@toa.io/core'
|
|
1
|
+
import type { bindings } from '@toa.io/core/types'
|
|
2
2
|
|
|
3
|
-
export function broadcast<T>
|
|
3
|
+
export function broadcast<T>(
|
|
4
|
+
name: string,
|
|
5
|
+
group?: string,
|
|
6
|
+
binding?: string
|
|
7
|
+
): bindings.Broadcast<T>
|
package/types/index.d.ts
CHANGED
|
@@ -1,21 +1,73 @@
|
|
|
1
|
-
import * as core from '@toa.io/core'
|
|
2
|
-
import * as
|
|
1
|
+
import type * as core from '@toa.io/core'
|
|
2
|
+
import type * as types from '@toa.io/core/types'
|
|
3
|
+
import type * as norm from '@toa.io/norm/types'
|
|
4
|
+
import * as composition from './composition.d.ts'
|
|
3
5
|
|
|
4
|
-
export * as bindings from './bindings'
|
|
6
|
+
export * as bindings from './bindings.d.ts'
|
|
5
7
|
|
|
6
|
-
export async function
|
|
8
|
+
export async function manifest(
|
|
9
|
+
path: string,
|
|
10
|
+
options?: composition.Options
|
|
11
|
+
): Promise<norm.Manifest>
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
/** What this process was given about the components it calls. */
|
|
14
|
+
export namespace map {
|
|
15
|
+
/** the file to read it from, or the map itself */
|
|
16
|
+
function use(value: string | Record<string, core.Contract> | undefined): void
|
|
9
17
|
|
|
10
|
-
|
|
18
|
+
/** what this process composes, stated by the boot that composes it */
|
|
19
|
+
function compose(manifests: norm.Manifest[]): Promise<void>
|
|
20
|
+
|
|
21
|
+
/** forgets what this process composed */
|
|
22
|
+
function forget(): void
|
|
23
|
+
|
|
24
|
+
function contract(id: string): Promise<core.Contract | undefined>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function composition(
|
|
28
|
+
paths: string[],
|
|
29
|
+
options?: composition.Options
|
|
30
|
+
): Promise<core.Connector>
|
|
31
|
+
|
|
32
|
+
export async function remote(
|
|
33
|
+
locator: core.Locator,
|
|
34
|
+
source?: types.Source
|
|
35
|
+
): Promise<core.Remote>
|
|
36
|
+
|
|
37
|
+
export async function receive<T = any>(
|
|
11
38
|
label: string,
|
|
12
|
-
receiver: Receiver
|
|
39
|
+
receiver: Receiver
|
|
13
40
|
): Promise<core.Connector>
|
|
14
41
|
|
|
15
|
-
export async function receive<T = any>
|
|
42
|
+
export async function receive<T = any>(
|
|
16
43
|
label: string,
|
|
17
44
|
group: string | undefined,
|
|
18
|
-
receiver: Receiver
|
|
45
|
+
receiver: Receiver
|
|
19
46
|
): Promise<core.Connector>
|
|
20
47
|
|
|
21
|
-
type Receiver = { receive: (message:
|
|
48
|
+
type Receiver = { receive: (message: types.Message<T>) => void | Promise<void> }
|
|
49
|
+
|
|
50
|
+
export function host(): types.extensions.Host
|
|
51
|
+
|
|
52
|
+
/** A process, as a connector: what the command built, and what the extensions keep in one. */
|
|
53
|
+
export class Workload extends core.Connector {
|
|
54
|
+
constructor(build: (workload: Workload) => Promise<core.Connector>)
|
|
55
|
+
|
|
56
|
+
/** A part of this process a halt takes down and builds again. */
|
|
57
|
+
gate(build: () => Promise<core.Connector>): core.Gate
|
|
58
|
+
|
|
59
|
+
/** Whether what a halt takes down is up. */
|
|
60
|
+
running(): boolean
|
|
61
|
+
|
|
62
|
+
/** Whether this process is quiet: it does nothing of its own accord and holds everything. */
|
|
63
|
+
quiescent(): boolean
|
|
64
|
+
|
|
65
|
+
/** Stops what this process does of its own accord, holding open everything it has. */
|
|
66
|
+
quiesce(): Promise<void>
|
|
67
|
+
|
|
68
|
+
/** Undoes a quiesce. */
|
|
69
|
+
cancel(): Promise<void>
|
|
70
|
+
|
|
71
|
+
/** Stops this process for `seconds`, then builds it again. */
|
|
72
|
+
stop(seconds: number): void
|
|
73
|
+
}
|
package/types/manifest.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as _norm from '@toa.io/norm/types'
|
|
2
|
-
import * as _composition from './composition'
|
|
2
|
+
import * as _composition from './composition.d.ts'
|
|
3
3
|
|
|
4
4
|
declare namespace toa.boot {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
type Manifest = (
|
|
6
|
+
path: string,
|
|
7
|
+
options?: _composition.Options
|
|
8
|
+
) => Promise<_norm.Component>
|
|
8
9
|
}
|
package/src/discovery.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { Discovery, Exposition } = require('@toa.io/core')
|
|
4
|
-
|
|
5
|
-
const boot = require('./index')
|
|
6
|
-
|
|
7
|
-
let promise
|
|
8
|
-
let instance = null
|
|
9
|
-
|
|
10
|
-
const discovery = async () => {
|
|
11
|
-
if (instance === null) {
|
|
12
|
-
instance = new Discovery(lookup)
|
|
13
|
-
promise = instance.connect()
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
await promise
|
|
17
|
-
|
|
18
|
-
return instance
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const lookup = async (locator) => {
|
|
22
|
-
const call = boot.call(locator, ENDPOINT, { bindings: BINDINGS })
|
|
23
|
-
|
|
24
|
-
await call.connect()
|
|
25
|
-
|
|
26
|
-
return call
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const expose = async (manifest) => {
|
|
30
|
-
const exposition = new Exposition(manifest.locator, manifest)
|
|
31
|
-
const operations = { [ENDPOINT]: { bindings: BINDINGS } }
|
|
32
|
-
const producers = boot.bindings.produce(exposition, operations)
|
|
33
|
-
|
|
34
|
-
await Promise.all(producers.map((producer) => producer.connect()))
|
|
35
|
-
|
|
36
|
-
return producers
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const BINDINGS = ['@toa.io/bindings.amqp']
|
|
40
|
-
const ENDPOINT = '.lookup'
|
|
41
|
-
|
|
42
|
-
exports.discovery = discovery
|
|
43
|
-
exports.expose = expose
|
package/src/exposition.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { Exposition, Locator } = require('@toa.io/core')
|
|
4
|
-
|
|
5
|
-
const boot = require('./index')
|
|
6
|
-
|
|
7
|
-
const exposition = async (manifest) => {
|
|
8
|
-
const locator = new Locator('', '')
|
|
9
|
-
const exposition = new Exposition(locator, manifest)
|
|
10
|
-
const producers = boot.bindings.expose(exposition)
|
|
11
|
-
|
|
12
|
-
await Promise.all(producers.map((producer) => producer.connect()))
|
|
13
|
-
|
|
14
|
-
return producers
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
exports.exposition = exposition
|