@toa.io/extensions.chronicles 0.0.0-stage → 1.0.0-alpha.332

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 (67) hide show
  1. package/components/chronicles.facts/manifest.toa.yaml +32 -0
  2. package/components/chronicles.facts/operations/record.js +4 -0
  3. package/components/chronicles.facts/types/index.d.ts +3 -0
  4. package/components/chronicles.facts/types/toa.d.ts +19 -0
  5. package/components/chronicles.ingestion/manifest.toa.yaml +60 -0
  6. package/components/chronicles.ingestion/operations/anonymous.js +4 -0
  7. package/components/chronicles.ingestion/operations/identified.js +4 -0
  8. package/components/chronicles.ingestion/types/index.d.ts +3 -0
  9. package/components/chronicles.ingestion/types/toa.d.ts +23 -0
  10. package/components/chronicles.queries/manifest.toa.yaml +115 -0
  11. package/components/chronicles.queries/operations/define.js +3 -0
  12. package/components/chronicles.queries/operations/describe.js +3 -0
  13. package/components/chronicles.queries/operations/drop.js +3 -0
  14. package/components/chronicles.queries/operations/query.js +3 -0
  15. package/components/chronicles.queries/types/index.d.ts +3 -0
  16. package/components/chronicles.queries/types/toa.d.ts +44 -0
  17. package/package.json +35 -4
  18. package/readme.md +369 -0
  19. package/transpiled/Client.d.ts +19 -0
  20. package/transpiled/Client.js +121 -0
  21. package/transpiled/Client.js.map +1 -0
  22. package/transpiled/Factory.d.ts +16 -0
  23. package/transpiled/Factory.js +53 -0
  24. package/transpiled/Factory.js.map +1 -0
  25. package/transpiled/Facts.d.ts +9 -0
  26. package/transpiled/Facts.js +16 -0
  27. package/transpiled/Facts.js.map +1 -0
  28. package/transpiled/Provider.d.ts +91 -0
  29. package/transpiled/Provider.js +4 -0
  30. package/transpiled/Provider.js.map +1 -0
  31. package/transpiled/Reader.d.ts +16 -0
  32. package/transpiled/Reader.js +82 -0
  33. package/transpiled/Reader.js.map +1 -0
  34. package/transpiled/Recorder.d.ts +28 -0
  35. package/transpiled/Recorder.js +82 -0
  36. package/transpiled/Recorder.js.map +1 -0
  37. package/transpiled/Scribe.d.ts +25 -0
  38. package/transpiled/Scribe.js +61 -0
  39. package/transpiled/Scribe.js.map +1 -0
  40. package/transpiled/Sender.d.ts +21 -0
  41. package/transpiled/Sender.js +69 -0
  42. package/transpiled/Sender.js.map +1 -0
  43. package/transpiled/Writer.d.ts +14 -0
  44. package/transpiled/Writer.js +50 -0
  45. package/transpiled/Writer.js.map +1 -0
  46. package/transpiled/address.d.ts +7 -0
  47. package/transpiled/address.js +27 -0
  48. package/transpiled/address.js.map +1 -0
  49. package/transpiled/clickhouse/Client.d.ts +38 -0
  50. package/transpiled/clickhouse/Client.js +73 -0
  51. package/transpiled/clickhouse/Client.js.map +1 -0
  52. package/transpiled/clickhouse/Provider.d.ts +33 -0
  53. package/transpiled/clickhouse/Provider.js +144 -0
  54. package/transpiled/clickhouse/Provider.js.map +1 -0
  55. package/transpiled/clickhouse/ddl.d.ts +26 -0
  56. package/transpiled/clickhouse/ddl.js +88 -0
  57. package/transpiled/clickhouse/ddl.js.map +1 -0
  58. package/transpiled/index.d.ts +9 -0
  59. package/transpiled/index.js +2 -0
  60. package/transpiled/index.js.map +1 -0
  61. package/transpiled/settings.d.ts +11 -0
  62. package/transpiled/settings.js +35 -0
  63. package/transpiled/settings.js.map +1 -0
  64. package/transpiled/store.d.ts +5 -0
  65. package/transpiled/store.js +11 -0
  66. package/transpiled/store.js.map +1 -0
  67. package/README.md +0 -3
package/readme.md ADDED
@@ -0,0 +1,369 @@
1
+ # Toa Chronicles
2
+
3
+ A log of what happens in a context, kept in a store the context names. Components write to it by
4
+ the events they name and the facts they send, browsers by a route of the gateway, and it is read
5
+ by an [HTTP API](#reading-the-log) — which the gateway also publishes as [MCP](#over-mcp) tools.
6
+ The service `chronicles-scribe` writes the log, and is what the store is provisioned by and read
7
+ through.
8
+
9
+ ## The log
10
+
11
+ Two tables.
12
+
13
+ **`events`** holds a row for everything that happened, from three sources:
14
+
15
+ | source | `label` | `entity`, `version` | `payload` |
16
+ | ------------------------------------------------ | --------------------- | ---------------------------------------- | ---------------------- |
17
+ | an [event](#events-and-states) a component names | `<component>.<event>` | the entity that changed, and its version | what the event renders |
18
+ | a [fact](#facts) an operation sends | `<component>.<name>` | `NULL` | what was sent |
19
+ | an event a [browser](#from-a-browser) sends | `agent.<name>` | `NULL` | what the browser sent |
20
+
21
+ An order placed, a search made and a page left are three rows:
22
+
23
+ | `label` | `component` | `entity` | `version` | `payload` |
24
+ | ----------------------- | ---------------------- | -------- | --------- | --------------------------------- |
25
+ | `store.orders.created` | `store.orders` | `9c1f…` | `0` | `{ total: 120, status: "new" }` |
26
+ | `store.search.searched` | `store.search` | `NULL` | `NULL` | `{ query: "teapot", results: 3 }` |
27
+ | `agent.page_left` | `chronicles.ingestion` | `NULL` | `NULL` | `{ path: "/pricing" }` |
28
+
29
+ Every row also has `id`, the same however often it is delivered, so a row is kept once;
30
+ `identity`, whose it is, where something [says so](#identity); `ip` and `country`, of the
31
+ browser that [sent it](#from-a-browser); `time`, when it happened; `received`, when the store wrote
32
+ it; and `trace`, the traceparent of the call that caused it.
33
+
34
+ **`states`** holds each entity as it stands: for every component that names a
35
+ [state](#events-and-states), a row per entity — `component`, `entity`, `version`, `identity`,
36
+ `time`, `received` and `payload` — which a higher version replaces.
37
+
38
+ Nothing is ordered: order by `time`, or by `version` within one entity.
39
+
40
+ ## Events and states
41
+
42
+ ```yaml
43
+ # manifest.toa.yaml
44
+ chronicles:
45
+ events: [created, paid] # what happened: rows of `events`
46
+ state: snapshot # the entity as it stands: its row of `states`
47
+ ```
48
+
49
+ Each label is an event of the component — its own, or the prototype's `created`, `updated`,
50
+ `deleted` and `sync`. An event is written as it renders: where its `condition` does not hold,
51
+ nothing is written; where it has a `payload`, what the payload returns is what is written. A field
52
+ a payload leaves out does not reach the store. A list is the `events` alone:
53
+
54
+ ```yaml
55
+ # manifest.toa.yaml
56
+ chronicles: [created, paid]
57
+ ```
58
+
59
+ The state is any event whose payload is the entity, `sync` among them. An event that exists for
60
+ the log alone is an event file like any other, and nothing else has to consume it:
61
+
62
+ ```typescript
63
+ // events/snapshot.ts
64
+ export function payload(event: Event) {
65
+ const { email, address, ...rest } = event.state
66
+
67
+ return rest
68
+ }
69
+ ```
70
+
71
+ A state's payload carries `id` and `VERSION`, which say which entity it is and which version: one
72
+ that does not is refused, and kept in the parking queue of its consumer rather than written. A row
73
+ of `events` takes its `entity`, `version` and `time` from the payload's `id`, `VERSION` and
74
+ `UPDATED` where it carries them, and is written without them, at the time it was taken, where it
75
+ does not. An event that is both is named in both.
76
+
77
+ A committed change is written however long the scribe is away, and once however often it is
78
+ delivered. One committed before the scribe first ran in a deployment is not.
79
+
80
+ ### Identity
81
+
82
+ `identity` is the payload's `identity`, where it is a string:
83
+
84
+ ```yaml
85
+ # manifest.toa.yaml
86
+ chronicles:
87
+ events: [created, paid]
88
+ state: snapshot
89
+ identity: customer # the property of the payloads that names whose a row is
90
+ ```
91
+
92
+ A component names another property, or none with `identity: ~`. One property serves every event of
93
+ the component and its state, and a payload that leaves it out is a row of no identity. A property
94
+ the component names is one of its entity.
95
+
96
+ ## Facts
97
+
98
+ ```typescript
99
+ await context.chronicles.send('searched', { query: input.query, results: results.length })
100
+ ```
101
+
102
+ The fact is a row of `events` under `<component>.searched`, with what was sent as its payload.
103
+ `send` resolves once the broker has it, and it then waits for the scribe as an event does.
104
+
105
+ An operation that knows whose the fact is says so; the extension does not know who called it:
106
+
107
+ ```typescript
108
+ await context.chronicles.send(
109
+ 'searched',
110
+ { query: input.query },
111
+ { identity: input.customer }
112
+ )
113
+ ```
114
+
115
+ `context.chronicles` is given to a component that declares `chronicles`. One that only sends
116
+ declares it empty:
117
+
118
+ ```yaml
119
+ # manifest.toa.yaml
120
+ chronicles: []
121
+ ```
122
+
123
+ ## From a browser
124
+
125
+ Served where the context says `ingest: true`, at two routes: one of an identity, and one of no one.
126
+
127
+ ### `POST /chronicles/ingestion/:identity/`
128
+
129
+ What a signed-in client sends, as the identity it is.
130
+
131
+ ```http
132
+ POST /chronicles/ingestion/9c1f0a6d5b7e4f3a8d2c1b0a9e8f7d6c/ HTTP/1.1
133
+ authorization: Token …
134
+ content-type: application/json
135
+
136
+ { "events": [{ "id": "0192f3a1-…", "name": "page_viewed", "time": 1761200000000, "payload": { "path": "/pricing" } }] }
137
+ ```
138
+
139
+ ```http
140
+ 202 Accepted
141
+
142
+ accepted: 1
143
+ ```
144
+
145
+ The token is the identity's, or the request is answered `403`: the rows are that identity's,
146
+ whatever the body says. Throttled to 60 requests a minute per identity.
147
+
148
+ ### `POST /chronicles/ingestion/`
149
+
150
+ What anyone sends, before signing in or without it. The same body, and rows of no identity; a body
151
+ that names one is refused.
152
+
153
+ ```http
154
+ POST /chronicles/ingestion/ HTTP/1.1
155
+ content-type: application/json
156
+
157
+ { "events": [{ "id": "0192f3a2-…", "name": "page_left", "time": 1761200000000 }] }
158
+ ```
159
+
160
+ It takes no credentials, and is throttled to 60 requests a minute per address. A form whose
161
+ `events` is their JSON is read as the JSON body, so `navigator.sendBeacon`, which sends no
162
+ `authorization`, can send a batch here as the page closes:
163
+
164
+ ```javascript
165
+ navigator.sendBeacon(
166
+ '/chronicles/ingestion/',
167
+ new URLSearchParams({ events: JSON.stringify(events) })
168
+ )
169
+ ```
170
+
171
+ A signed-in client sends with `fetch(…, { keepalive: true })` instead. Anyone can send anything: a
172
+ `user` in a payload is what the client said.
173
+
174
+ ### The body
175
+
176
+ `{ events }`, at most 100 of them, each a row of `events` under `agent.<name>`:
177
+
178
+ | property | |
179
+ | --------- | ------------------------------------------------------------------- |
180
+ | `id` | the client's own, at most 64 characters; a retry sends the same one |
181
+ | `name` | lowercase letters, digits and underscores from a letter, at most 64 |
182
+ | `time` | when it happened, in milliseconds, by the client's clock |
183
+ | `payload` | an object, at most 16 KiB of JSON |
184
+
185
+ A request is answered once its events are written. A batch one event of which breaks any of the
186
+ above is written not at all, and answered `422` with the code `INVALID`, or `OVERSIZED` for a
187
+ payload too large, and the `index` of the event in `cause`.
188
+
189
+ ### Address and country
190
+
191
+ Every row a browser sent has the `ip` and the `country` of the request, as the gateway reads them
192
+ from the headers [`exposition.ip` and `exposition.country`](/extensions/exposition/documentation/ip.md)
193
+ name, and `NULL` where it has none; a body that names either is overwritten. An address is
194
+ personal data, and the log erases nothing, so of an address the log keeps its network — a `/24` of
195
+ IPv4, a `/48` of IPv6 — unless the annotation says `ip: full`.
196
+
197
+ ## Reading the log
198
+
199
+ Reading takes the role `system:chronicles:query`, and views take `system:chronicles:views`;
200
+ `system:chronicles` grants both. A refusal is answered `422` with `{ code, cause }`.
201
+
202
+ ### `GET /chronicles/queries/`
203
+
204
+ What the log holds: the dialect queries are written in, the labels of `events`, the components
205
+ with a state, and the views.
206
+
207
+ ```http
208
+ GET /chronicles/queries/ HTTP/1.1
209
+ authorization: Token …
210
+ accept: application/yaml
211
+ ```
212
+
213
+ ```http
214
+ 200 OK
215
+
216
+ dialect: ClickHouse SQL; read `events` and `states` with FINAL
217
+ labels:
218
+ - label: store.orders.created
219
+ rows: 1204
220
+ first: 2026-10-01 08:12:03.000
221
+ last: 2026-10-24 17:40:51.000
222
+ paths: [VERSION, id, status, total]
223
+ states:
224
+ - component: store.orders
225
+ entities: 1180
226
+ paths: [VERSION, id, status, total]
227
+ views:
228
+ - name: daily
229
+ sql: SELECT toDate(time) AS day, count() AS orders FROM events FINAL GROUP BY day
230
+ refresh: 3600
231
+ ```
232
+
233
+ `rows` counts what the store holds, a row delivered twice possibly twice; `paths` are the fields of
234
+ the latest payloads.
235
+
236
+ ### `POST /chronicles/queries/`
237
+
238
+ One statement, in the dialect.
239
+
240
+ ```http
241
+ POST /chronicles/queries/ HTTP/1.1
242
+ authorization: Token …
243
+ accept: application/yaml
244
+ content-type: application/yaml
245
+
246
+ sql: SELECT payload.status AS status, count() AS n FROM states FINAL GROUP BY status
247
+ ```
248
+
249
+ ```http
250
+ 201 Created
251
+
252
+ columns:
253
+ - name: status
254
+ type: Dynamic
255
+ - name: n
256
+ type: UInt64
257
+ rows:
258
+ - [new, 31]
259
+ - [paid, 1149]
260
+ truncated: false
261
+ ```
262
+
263
+ A query reads the log and nothing else, and cannot write. It runs for at most 30 seconds and
264
+ answers at most 10,000 rows: at the rows it answers what it has, with `truncated: true`. It leaves
265
+ `FORMAT` out.
266
+
267
+ | code | when |
268
+ | --------- | ----------------------------------------------------------------------- |
269
+ | `REFUSED` | the store refused the statement; `cause.message` is what the store said |
270
+
271
+ ### `PUT /chronicles/queries/views/:name`
272
+
273
+ A view over the log, by a statement a query could run. Defining a name again replaces the view.
274
+
275
+ ```http
276
+ PUT /chronicles/queries/views/daily/ HTTP/1.1
277
+ authorization: Token …
278
+ content-type: application/yaml
279
+
280
+ sql: SELECT toDate(time) AS day, count() AS orders FROM events FINAL GROUP BY day
281
+ refresh: 3600
282
+ ```
283
+
284
+ ```http
285
+ 204 No Content
286
+ ```
287
+
288
+ Without `refresh`, a view is computed when it is read. With `refresh`, in seconds and at least
289
+ `3600`, it is computed on that cycle and read as a table. A name is lowercase letters, digits and
290
+ underscores, and a context has at most 50 views. A query reads a view by its name.
291
+
292
+ | code | when |
293
+ | ----------- | -------------------------------------------------------------------- |
294
+ | `REFUSED` | the statement is not one a query could run; `cause.message` says why |
295
+ | `RESERVED` | the name is one of the log's tables |
296
+ | `EXHAUSTED` | the context has 50 views, and this is not one of them |
297
+
298
+ ### `DELETE /chronicles/queries/views/:name`
299
+
300
+ ```http
301
+ DELETE /chronicles/queries/views/daily/ HTTP/1.1
302
+ authorization: Token …
303
+ ```
304
+
305
+ ```http
306
+ 204 No Content
307
+ ```
308
+
309
+ | code | when |
310
+ | ----------- | --------------------- |
311
+ | `NOT_FOUND` | there is no such view |
312
+
313
+ ### Over MCP
314
+
315
+ Where the context publishes [MCP](/extensions/exposition/documentation/mcp.md), each of these
316
+ routes is a tool, under the name the gateway gives a route, and taking what the route takes:
317
+
318
+ | tool | route |
319
+ | --------------------------------------- | ---------------------------------------- |
320
+ | `chronicles.queries.GET` | `GET /chronicles/queries/` |
321
+ | `chronicles.queries.POST` | `POST /chronicles/queries/` |
322
+ | `chronicles.queries.views._name.PUT` | `PUT /chronicles/queries/views/:name` |
323
+ | `chronicles.queries.views._name.DELETE` | `DELETE /chronicles/queries/views/:name` |
324
+
325
+ A tool is listed to a caller that holds its role, and a refusal is a tool result with `isError`.
326
+
327
+ ## Deployment
328
+
329
+ ```yaml
330
+ # context.toa.yaml
331
+ chronicles:
332
+ store:
333
+ provider: clickhouse
334
+ url: https://ch.example.com:8443/shop
335
+ retention: 730
336
+ ingest: true
337
+ ```
338
+
339
+ | | |
340
+ | ----------- | ----------------------------------------------------------- |
341
+ | `store` | the provider that keeps the log, and its address |
342
+ | `retention` | days a row of `events` is kept; absent, kept |
343
+ | `ingest` | the browser route; absent, there is none |
344
+ | `ip` | `full` keeps a browser's whole address; absent, its network |
345
+
346
+ ```shell
347
+ $ toa conceal chronicles-store username=… password=… reader=…
348
+ ```
349
+
350
+ The user provisions the store when the service starts. It creates the reader, the user queries run
351
+ as, with `reader` as its password.
352
+
353
+ The events the components name travel where the context's `amqp` says, as every event does.
354
+
355
+ ## ClickHouse
356
+
357
+ `url` is ClickHouse's HTTP address, and its path names the database, which is created where it does
358
+ not exist. The user needs `CREATE`, `INSERT` and `access_management`; the reader is
359
+ `<database>_reader`.
360
+
361
+ `events` and `states` are replaced by key in the background, so a query reads them with `FINAL`,
362
+ which collapses what was delivered twice:
363
+
364
+ ```sql
365
+ SELECT payload.total::Float64, count() FROM events FINAL WHERE label = 'store.orders.paid' GROUP BY 1
366
+ SELECT entity, payload FROM states FINAL WHERE component = 'store.orders'
367
+ ```
368
+
369
+ A view with `refresh` is a refreshable materialized view.
@@ -0,0 +1,19 @@
1
+ import { Connector } from '@toa.io/core';
2
+ import type { Address } from '@toa.io/definitions/extensions.chronicles';
3
+ import type { Writer } from './Writer.ts';
4
+ /**
5
+ * What `chronicles.ingestion` writes what browsers send through: each event under `agent.<name>`,
6
+ * with the client's own id and time, all of a batch in one write, and answered once it is.
7
+ */
8
+ export declare class Client extends Connector {
9
+ readonly name = "chronicles";
10
+ private readonly writer;
11
+ private readonly address;
12
+ constructor(writer: () => Writer, address: () => Address);
13
+ invoke(method: string, ...args: any[]): Promise<unknown>;
14
+ /**
15
+ * `identity` is the one the gateway verified the caller to be, from the path; `null` for the
16
+ * route anyone may call, whose body is then refused where it says whose it is.
17
+ */
18
+ private ingest;
19
+ }
@@ -0,0 +1,121 @@
1
+ import { current, encode } from 'openspan';
2
+ import { Connector, exceptions } from '@toa.io/core';
3
+ import { AGENT, INGESTION, NAMESPACE } from '@toa.io/definitions/extensions.chronicles';
4
+ import { keep } from './address.js';
5
+ /**
6
+ * What `chronicles.ingestion` writes what browsers send through: each event under `agent.<name>`,
7
+ * with the client's own id and time, all of a batch in one write, and answered once it is.
8
+ */
9
+ export class Client extends Connector {
10
+ name = 'chronicles';
11
+ writer;
12
+ address;
13
+ constructor(writer, address) {
14
+ super();
15
+ this.writer = writer;
16
+ this.address = address;
17
+ }
18
+ async invoke(method, ...args) {
19
+ if (method !== 'ingest')
20
+ throw new exceptions.MisuseException(`Chronicles have no '${method}'`);
21
+ return await this.ingest(args[0], args[1] ?? null);
22
+ }
23
+ /**
24
+ * `identity` is the one the gateway verified the caller to be, from the path; `null` for the
25
+ * route anyone may call, whose body is then refused where it says whose it is.
26
+ */
27
+ async ingest(input, identity) {
28
+ const events = parse(input, identity !== null);
29
+ if (events instanceof Error)
30
+ return events;
31
+ const context = current();
32
+ const trace = context === undefined ? null : encode(context);
33
+ // put in by the gateway, which overwrites whatever the body says of them
34
+ const { ip, country } = input;
35
+ const address = keep(ip, this.address());
36
+ const from = typeof country === 'string' ? country : null;
37
+ const rows = events.map((event) => ({
38
+ id: event.id,
39
+ label: `${AGENT}.${event.name}`,
40
+ component: COMPONENT,
41
+ time: event.time,
42
+ entity: null,
43
+ version: null,
44
+ identity,
45
+ ip: address,
46
+ country: from,
47
+ trace,
48
+ payload: event.payload ?? {}
49
+ }));
50
+ await this.writer().write(rows);
51
+ return { status: 202, accepted: rows.length };
52
+ }
53
+ }
54
+ /**
55
+ * A batch, `{ events }`: as JSON, or as the form `navigator.sendBeacon` sends, whose `events` is
56
+ * the JSON of them. Checked here rather than by the operation's schema, which cannot read the
57
+ * form's. `identified` is whether the gateway put the identity in, which is then a key the body
58
+ * may have beside `events`.
59
+ */
60
+ function parse(input, identified) {
61
+ if (typeof input !== 'object' || input === null || Array.isArray(input))
62
+ return invalid('The body is `{ events }`');
63
+ // `ip` and `country` are the gateway's, which sets them on every request it makes
64
+ const { events: given, identity, ip: _ip, country: _country, ...rest } = input;
65
+ const extra = Object.keys(rest);
66
+ if (extra.length > 0)
67
+ return invalid(`The body has no '${extra[0]}'`);
68
+ // the identity is the path's, never the body's
69
+ if (!identified && identity !== undefined)
70
+ return invalid('The body names no identity: a client that has one sends it in the path');
71
+ let events = given;
72
+ if (typeof events === 'string')
73
+ try {
74
+ events = JSON.parse(events);
75
+ }
76
+ catch {
77
+ return invalid('`events` is not JSON');
78
+ }
79
+ if (!Array.isArray(events))
80
+ return invalid('`events` is an array of events');
81
+ if (events.length > MAX_EVENTS)
82
+ return invalid(`A request carries at most ${MAX_EVENTS} events`);
83
+ for (const [index, event] of events.entries()) {
84
+ const problem = check(event);
85
+ if (problem !== null)
86
+ return invalid(problem, index);
87
+ if (JSON.stringify(event.payload ?? {}).length > MAX_PAYLOAD)
88
+ return new Error('OVERSIZED', {
89
+ cause: { index, message: `A payload is at most ${MAX_PAYLOAD} bytes of JSON` }
90
+ });
91
+ }
92
+ return events;
93
+ }
94
+ function check(event) {
95
+ if (typeof event !== 'object' || event === null || Array.isArray(event))
96
+ return 'An event is an object';
97
+ const { id, name, time, payload, ...rest } = event;
98
+ if (Object.keys(rest).length > 0)
99
+ return `An event has no '${Object.keys(rest)[0]}'`;
100
+ if (typeof id !== 'string' || id.length === 0 || id.length > 64)
101
+ return '`id` is a string of 1 to 64 characters';
102
+ if (typeof name !== 'string' || name.length > 64 || !NAME.test(name))
103
+ return '`name` is lowercase letters, digits and underscores from a letter, at most 64 characters';
104
+ if (!Number.isInteger(time) || time < 0)
105
+ return '`time` is milliseconds since the epoch';
106
+ if (payload !== undefined &&
107
+ (typeof payload !== 'object' || payload === null || Array.isArray(payload)))
108
+ return '`payload` is an object';
109
+ return null;
110
+ }
111
+ function invalid(message, index) {
112
+ return new Error('INVALID', {
113
+ cause: index === undefined ? { message } : { index, message }
114
+ });
115
+ }
116
+ const COMPONENT = `${NAMESPACE}.${INGESTION}`;
117
+ const MAX_EVENTS = 100;
118
+ const MAX_PAYLOAD = 16 * 1024;
119
+ // no dot: a label is `agent.<name>`, and what follows the prefix is one name
120
+ const NAME = /^[a-z][a-z0-9_]*$/;
121
+ //# sourceMappingURL=Client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Client.js","sourceRoot":"","sources":["../source/Client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { Connector, Locator } from '@toa.io/core';
2
+ import type { extensions } from '@toa.io/core/types';
3
+ /**
4
+ * A component sends facts through its context; the events it names reach the log as events of
5
+ * the context, which the service consumes. The components this extension ships are given what
6
+ * they serve instead: the queries, the provider; the facts and the client route, the writer.
7
+ */
8
+ export declare class Factory implements extensions.Factory {
9
+ private readonly host;
10
+ /** one store and one writer for the process, made where the process is one that keeps the log */
11
+ private store;
12
+ constructor(host: extensions.Host);
13
+ aspect(locator: Locator): extensions.Aspect;
14
+ service(): Connector;
15
+ private keep;
16
+ }
@@ -0,0 +1,53 @@
1
+ import { FACTS, INGESTION, NAMESPACE, QUERIES } from '@toa.io/definitions/extensions.chronicles';
2
+ import { Client } from './Client.js';
3
+ import { Facts } from './Facts.js';
4
+ import { Reader } from './Reader.js';
5
+ import { Scribe } from './Scribe.js';
6
+ import { Sender } from './Sender.js';
7
+ import { Writer } from './Writer.js';
8
+ import { credentials, settings } from './settings.js';
9
+ import { provider } from './store.js';
10
+ /**
11
+ * A component sends facts through its context; the events it names reach the log as events of
12
+ * the context, which the service consumes. The components this extension ships are given what
13
+ * they serve instead: the queries, the provider; the facts and the client route, the writer.
14
+ */
15
+ export class Factory {
16
+ host;
17
+ /** one store and one writer for the process, made where the process is one that keeps the log */
18
+ store;
19
+ constructor(host) {
20
+ this.host = host;
21
+ }
22
+ aspect(locator) {
23
+ if (mine(locator))
24
+ switch (locator.name) {
25
+ case QUERIES:
26
+ return new Reader(() => this.keep().provider);
27
+ case FACTS:
28
+ return new Facts(() => this.keep().writer);
29
+ case INGESTION:
30
+ return new Client(() => this.keep().writer, () => settings().ip);
31
+ }
32
+ return new Sender(this.host, locator);
33
+ }
34
+ // nothing connects to this, so a halt takes the whole of it
35
+ service() {
36
+ return this.host.gate(async () => {
37
+ const { provider, writer } = this.keep();
38
+ return new Scribe(this.host, settings(), provider, writer);
39
+ });
40
+ }
41
+ keep() {
42
+ if (this.store === undefined) {
43
+ const store = provider(settings(), credentials());
44
+ this.store = { provider: store, writer: new Writer(store) };
45
+ }
46
+ return this.store;
47
+ }
48
+ }
49
+ /** Whether this is a component the extension ships rather than one of the application's. */
50
+ function mine(locator) {
51
+ return locator.namespace === NAMESPACE;
52
+ }
53
+ //# sourceMappingURL=Factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Factory.js","sourceRoot":"","sources":["../source/Factory.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EACL,SAAS,EACT,SAAS,EACT,OAAO,EACR,MAAM,2CAA2C,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAKrC;;;;GAIG;AACH,MAAM,OAAO,OAAO;IACD,IAAI,CAAiB;IAEtC,iGAAiG;IACzF,KAAK,CAAoD;IAEjE,YAAmB,IAAqB;QACtC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;IAEM,MAAM,CAAC,OAAgB;QAC5B,IAAI,IAAI,CAAC,OAAO,CAAC;YACf,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;gBACrB,KAAK,OAAO;oBACV,OAAO,IAAI,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,CAAA;gBAC/C,KAAK,KAAK;oBACR,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAA;gBAC5C,KAAK,SAAS;oBACZ,OAAO,IAAI,MAAM,CACf,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,EACxB,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,EAAE,CACpB,CAAA;YACL,CAAC;QAEH,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IACvC,CAAC;IAED,4DAA4D;IACrD,OAAO;QACZ,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YAC/B,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;YAExC,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAA;QAC5D,CAAC,CAAC,CAAA;IACJ,CAAC;IAEO,IAAI;QACV,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,EAAE,EAAE,WAAW,EAAE,CAAC,CAAA;YAEjD,IAAI,CAAC,KAAK,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAA;QAC7D,CAAC;QAED,OAAO,IAAI,CAAC,KAAK,CAAA;IACnB,CAAC;CACF;AAED,4FAA4F;AAC5F,SAAS,IAAI,CAAC,OAAgB;IAC5B,OAAO,OAAO,CAAC,SAAS,KAAK,SAAS,CAAA;AACxC,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { Connector } from '@toa.io/core';
2
+ import type { Writer } from './Writer.ts';
3
+ /** What `chronicles.facts` writes a fact a component sent through. */
4
+ export declare class Facts extends Connector {
5
+ readonly name = "chronicles";
6
+ private readonly writer;
7
+ constructor(writer: () => Writer);
8
+ invoke(method: string, ...args: any[]): Promise<void>;
9
+ }
@@ -0,0 +1,16 @@
1
+ import { Connector, exceptions } from '@toa.io/core';
2
+ /** What `chronicles.facts` writes a fact a component sent through. */
3
+ export class Facts extends Connector {
4
+ name = 'chronicles';
5
+ writer;
6
+ constructor(writer) {
7
+ super();
8
+ this.writer = writer;
9
+ }
10
+ async invoke(method, ...args) {
11
+ if (method !== 'record')
12
+ throw new exceptions.MisuseException(`Chronicles have no '${method}'`);
13
+ await this.writer().write([args[0]]);
14
+ }
15
+ }
16
+ //# sourceMappingURL=Facts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Facts.js","sourceRoot":"","sources":["../source/Facts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAIpD,sEAAsE;AACtE,MAAM,OAAO,KAAM,SAAQ,SAAS;IAClB,IAAI,GAAG,YAAY,CAAA;IAElB,MAAM,CAAc;IAErC,YAAmB,MAAoB;QACrC,KAAK,EAAE,CAAA;QAEP,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IACtB,CAAC;IAEM,KAAK,CAAC,MAAM,CAAC,MAAc,EAAE,GAAG,IAAW;QAChD,IAAI,MAAM,KAAK,QAAQ;YACrB,MAAM,IAAI,UAAU,CAAC,eAAe,CAAC,uBAAuB,MAAM,GAAG,CAAC,CAAA;QAExE,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAQ,CAAC,CAAC,CAAA;IAC7C,CAAC;CACF"}