@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.
- package/components/chronicles.facts/manifest.toa.yaml +32 -0
- package/components/chronicles.facts/operations/record.js +4 -0
- package/components/chronicles.facts/types/index.d.ts +3 -0
- package/components/chronicles.facts/types/toa.d.ts +19 -0
- package/components/chronicles.ingestion/manifest.toa.yaml +60 -0
- package/components/chronicles.ingestion/operations/anonymous.js +4 -0
- package/components/chronicles.ingestion/operations/identified.js +4 -0
- package/components/chronicles.ingestion/types/index.d.ts +3 -0
- package/components/chronicles.ingestion/types/toa.d.ts +23 -0
- package/components/chronicles.queries/manifest.toa.yaml +115 -0
- package/components/chronicles.queries/operations/define.js +3 -0
- package/components/chronicles.queries/operations/describe.js +3 -0
- package/components/chronicles.queries/operations/drop.js +3 -0
- package/components/chronicles.queries/operations/query.js +3 -0
- package/components/chronicles.queries/types/index.d.ts +3 -0
- package/components/chronicles.queries/types/toa.d.ts +44 -0
- package/package.json +35 -4
- package/readme.md +369 -0
- package/transpiled/Client.d.ts +19 -0
- package/transpiled/Client.js +121 -0
- package/transpiled/Client.js.map +1 -0
- package/transpiled/Factory.d.ts +16 -0
- package/transpiled/Factory.js +53 -0
- package/transpiled/Factory.js.map +1 -0
- package/transpiled/Facts.d.ts +9 -0
- package/transpiled/Facts.js +16 -0
- package/transpiled/Facts.js.map +1 -0
- package/transpiled/Provider.d.ts +91 -0
- package/transpiled/Provider.js +4 -0
- package/transpiled/Provider.js.map +1 -0
- package/transpiled/Reader.d.ts +16 -0
- package/transpiled/Reader.js +82 -0
- package/transpiled/Reader.js.map +1 -0
- package/transpiled/Recorder.d.ts +28 -0
- package/transpiled/Recorder.js +82 -0
- package/transpiled/Recorder.js.map +1 -0
- package/transpiled/Scribe.d.ts +25 -0
- package/transpiled/Scribe.js +61 -0
- package/transpiled/Scribe.js.map +1 -0
- package/transpiled/Sender.d.ts +21 -0
- package/transpiled/Sender.js +69 -0
- package/transpiled/Sender.js.map +1 -0
- package/transpiled/Writer.d.ts +14 -0
- package/transpiled/Writer.js +50 -0
- package/transpiled/Writer.js.map +1 -0
- package/transpiled/address.d.ts +7 -0
- package/transpiled/address.js +27 -0
- package/transpiled/address.js.map +1 -0
- package/transpiled/clickhouse/Client.d.ts +38 -0
- package/transpiled/clickhouse/Client.js +73 -0
- package/transpiled/clickhouse/Client.js.map +1 -0
- package/transpiled/clickhouse/Provider.d.ts +33 -0
- package/transpiled/clickhouse/Provider.js +144 -0
- package/transpiled/clickhouse/Provider.js.map +1 -0
- package/transpiled/clickhouse/ddl.d.ts +26 -0
- package/transpiled/clickhouse/ddl.js +88 -0
- package/transpiled/clickhouse/ddl.js.map +1 -0
- package/transpiled/index.d.ts +9 -0
- package/transpiled/index.js +2 -0
- package/transpiled/index.js.map +1 -0
- package/transpiled/settings.d.ts +11 -0
- package/transpiled/settings.js +35 -0
- package/transpiled/settings.js.map +1 -0
- package/transpiled/store.d.ts +5 -0
- package/transpiled/store.js +11 -0
- package/transpiled/store.js.map +1 -0
- 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"}
|