@bazariodev/docket 0.0.0 → 1.0.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.
- package/CHANGELOG.md +36 -0
- package/README.md +715 -173
- package/dist/{chunk-B-SP8Od_.js → chunk-DkvWLziD.js} +3 -3
- package/dist/{chunk-B-SP8Od_.js.map → chunk-DkvWLziD.js.map} +1 -1
- package/dist/{chunk-Bvq4tF_F.cjs → chunk-ieB8yTEn.cjs} +2 -3
- package/dist/{chunk-Bvq4tF_F.cjs.map → chunk-ieB8yTEn.cjs.map} +1 -1
- package/dist/client/index.cjs +167 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +40 -0
- package/dist/client/index.d.ts +40 -0
- package/dist/client/index.js +165 -0
- package/dist/client/index.js.map +1 -0
- package/dist/constants.d.cts +2 -4
- package/dist/constants.d.ts +2 -4
- package/dist/export/index.cjs +39 -11
- package/dist/export/index.cjs.map +1 -1
- package/dist/export/index.js +39 -11
- package/dist/export/index.js.map +1 -1
- package/dist/history/index.cjs +21 -1187
- package/dist/history/index.cjs.map +1 -1
- package/dist/history/index.d.cts +10 -112
- package/dist/history/index.d.ts +10 -112
- package/dist/history/index.js +22 -1188
- package/dist/history/index.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/indexeddb/index.cjs +18 -7
- package/dist/indexeddb/index.cjs.map +1 -1
- package/dist/indexeddb/index.d.cts +19 -4
- package/dist/indexeddb/index.d.ts +19 -4
- package/dist/indexeddb/index.js +18 -8
- package/dist/indexeddb/index.js.map +1 -1
- package/dist/storage/contracts.d.cts +0 -4
- package/dist/storage/contracts.d.ts +0 -4
- package/dist/upload/constants.d.cts +17 -0
- package/dist/upload/constants.d.ts +17 -0
- package/dist/upload/index.cjs +246 -0
- package/dist/upload/index.cjs.map +1 -0
- package/dist/upload/index.d.cts +51 -0
- package/dist/upload/index.d.ts +51 -0
- package/dist/upload/index.js +244 -0
- package/dist/upload/index.js.map +1 -0
- package/package.json +25 -2
- package/dist/core/errors.d.cts +0 -8
- package/dist/core/errors.d.ts +0 -8
- package/dist/history/constants.d.cts +0 -35
- package/dist/history/constants.d.ts +0 -35
- package/dist/sip/constants.d.cts +0 -5
- package/dist/sip/constants.d.ts +0 -5
package/README.md
CHANGED
|
@@ -9,51 +9,357 @@ and build ZIP files for download or application upload.
|
|
|
9
9
|
pnpm add @bazariodev/docket
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
Docket
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
Docket uses [Coordination](https://www.npmjs.com/package/@bazariodev/coordination) as a runtime
|
|
13
|
+
dependency. The `/history` entry creates its private `Queue` directly. The root entry has no
|
|
14
|
+
external runtime imports. Public declarations use local types and structural provider ports.
|
|
15
|
+
Memory history, upload, conversion, persistence, SIP, and ZIP support are optional entries.
|
|
16
|
+
Import only the entries that the application uses.
|
|
16
17
|
|
|
17
18
|
The usage example selects [Winnow](https://www.npmjs.com/package/@bazariodev/winnow) for bounded
|
|
18
|
-
conversion
|
|
19
|
+
conversion:
|
|
19
20
|
|
|
20
21
|
```sh
|
|
21
|
-
pnpm add @bazariodev/winnow
|
|
22
|
+
pnpm add @bazariodev/winnow
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
Other providers can implement the same structural ports. A selected provider can install its own
|
|
25
26
|
dependencies. Docket does not change that provider's package graph. Optional examples below also
|
|
26
|
-
select Baler for ZIP files and
|
|
27
|
+
select [Baler](https://www.npmjs.com/package/@bazariodev/baler) for ZIP files and
|
|
28
|
+
[Hopper](https://www.npmjs.com/package/@bazariodev/hopper) for standalone persistence.
|
|
27
29
|
Install these packages only for the corresponding application composition.
|
|
28
30
|
|
|
29
31
|
Node.js support is `^22.12.0 || ^24.0.0 || >=26.0.0`. Browser persistence needs the selected storage
|
|
30
32
|
provider's platform APIs. The Hopper IndexedDB composition uses IndexedDB and Web Locks.
|
|
31
|
-
|
|
33
|
+
Memory history and memory outboxes use no device storage or application lease.
|
|
32
34
|
|
|
33
35
|
## Usage
|
|
34
36
|
|
|
35
37
|
```js
|
|
36
|
-
import {
|
|
38
|
+
import { createDocket } from '@bazariodev/docket/client';
|
|
37
39
|
import { createRedactor } from '@bazariodev/docket/redaction';
|
|
38
|
-
import { createMemoryStore } from '@bazariodev/hopper';
|
|
39
40
|
import * as conversion from '@bazariodev/winnow';
|
|
41
|
+
|
|
40
42
|
const redactor = createRedactor(
|
|
41
43
|
{ fields: { password: 'drop', accessToken: 'drop' } },
|
|
42
44
|
conversion,
|
|
43
45
|
);
|
|
44
|
-
const
|
|
46
|
+
const docket = createDocket({
|
|
47
|
+
areas: [{ id: 'application' }, { id: 'network', level: 'warn' }],
|
|
48
|
+
redactor,
|
|
49
|
+
});
|
|
50
|
+
docket.getLogger('application', 'Connection').info('Connection ready');
|
|
51
|
+
const records = await docket.history?.query();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This call keeps the last 1,000 records in memory. Console output and upload are off.
|
|
55
|
+
It uses no device storage.
|
|
56
|
+
Supply your application's area list and privacy policy. Default text rules do not identify every
|
|
57
|
+
private field in an application.
|
|
58
|
+
|
|
59
|
+
| Task | Example or guidance |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| Keep and query local history | [Memory history](#memory-history). |
|
|
62
|
+
| Add console output or a custom sink | [Console output and custom sinks](#console-output-and-custom-sinks). |
|
|
63
|
+
| Keep history and upload independently | [Storage and delivery options](#storage-and-delivery-options). |
|
|
64
|
+
| Add correlation context, Errors, or lazy data | [Context, Errors, and lazy data](#context-errors-and-lazy-data). |
|
|
65
|
+
| Produce JSON or NDJSON | [JSON and NDJSON](#json-and-ndjson). |
|
|
66
|
+
| Select capacity and bound input cost | [Capacity and input cost](#capacity-and-input-cost). |
|
|
67
|
+
| Flush, stop, erase, and remove browser listeners | [Shutdown and account changes](#shutdown-and-account-changes). |
|
|
68
|
+
|
|
69
|
+
## Storage and delivery options
|
|
70
|
+
|
|
71
|
+
History and upload are independent. No strategy name or storage registry is required.
|
|
72
|
+
|
|
73
|
+
| Options | Result |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| Neither option | Cyclic memory history; no upload. |
|
|
76
|
+
| `history: { maxRecords: 2000 }` | Cyclic memory history with another capacity. |
|
|
77
|
+
| `history: historyClient` | Use a supplied memory, IndexedDB, or other compatible client. |
|
|
78
|
+
| `history: false`, `upload: { ... }` | Upload only. |
|
|
79
|
+
| `history: { ... }`, `upload: { ... }` | History and upload. |
|
|
80
|
+
| `upload: false` or omitted upload | No upload. |
|
|
81
|
+
|
|
82
|
+
Supply Hopper clients directly, or supply other clients with the same public methods.
|
|
83
|
+
Open persistent clients only after authentication. The application supplies an opaque
|
|
84
|
+
`authenticatedSession.storageKey` and an `uploadRecords` callback bound to that same account.
|
|
85
|
+
Use a different scope for each tenant and account:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
import { createIndexedDbStore } from '@bazariodev/hopper/indexeddb';
|
|
89
|
+
import { createIndexedDbOutbox } from '@bazariodev/hopper/outbox/indexeddb';
|
|
90
|
+
|
|
91
|
+
const scope = authenticatedSession.storageKey;
|
|
92
|
+
const names = (kind) => ({
|
|
93
|
+
databaseName: JSON.stringify(['diagnostics', scope, kind]),
|
|
94
|
+
lockName: JSON.stringify(['diagnostics-owner', scope, kind]),
|
|
95
|
+
});
|
|
96
|
+
const storageOptions = { history: names('history'), outbox: names('outbox') };
|
|
97
|
+
const historyClient = createIndexedDbStore(storageOptions.history);
|
|
98
|
+
const outbox = await createIndexedDbOutbox({
|
|
99
|
+
...storageOptions.outbox,
|
|
100
|
+
maxRecords: 1000,
|
|
101
|
+
});
|
|
102
|
+
const persistent = createDocket({
|
|
103
|
+
areas: [{ id: 'application' }],
|
|
104
|
+
redactor,
|
|
105
|
+
history: historyClient,
|
|
106
|
+
upload: {
|
|
107
|
+
outbox,
|
|
108
|
+
send: uploadRecords,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The client selects storage. Docket imports no Hopper code. For memory upload, supply
|
|
114
|
+
`createMemoryOutbox({ maxRecords })` from `@bazariodev/hopper/outbox`. The outbox is required;
|
|
115
|
+
its factory owns capacity and storage configuration. Docket does not copy those options.
|
|
116
|
+
|
|
117
|
+
A history client needs `write`, `query`, and `clear`; `flush`, `stop`, and `getStatus` are optional.
|
|
118
|
+
An outbox needs the public upload port plus `clear` and `stop`. Methods keep their receivers.
|
|
119
|
+
The returned `history` and `outbox` are the supplied clients. Do not use one client for both
|
|
120
|
+
roles or share owned clients between compositions. A successful `createDocket` call owns their
|
|
121
|
+
shutdown. Failed construction leaves supplied clients open and cancels its new upload timer.
|
|
122
|
+
|
|
123
|
+
History keeps records for download. The outbox keeps records until server acceptance. They are
|
|
124
|
+
separate stores: upload success must not erase local history. Clearing history must not remove
|
|
125
|
+
pending upload records. Current Hopper history and outbox schemas need separate databases.
|
|
126
|
+
Database and lock names come from initial client options. Use the same lock name for all writers
|
|
127
|
+
of one database. Hopper history keeps its timestamp-order and identity rules; the default cyclic
|
|
128
|
+
history keeps FIFO order and duplicate admissions.
|
|
129
|
+
|
|
130
|
+
Upload defaults to 100 records per batch and a 5,000 ms interval. Supply
|
|
131
|
+
`send(records, signal, token)`. The consumer owns the endpoint, credentials, encoding,
|
|
132
|
+
request timeout, retry, response checks, and transport error reporting. Docket contains no HTTP client.
|
|
133
|
+
|
|
134
|
+
Resolve the callback only after the backend accepts the complete batch. After handling a failure,
|
|
135
|
+
throw or reject so Docket retains the batch and token. Catching an error and resolving reports
|
|
136
|
+
success and permits acknowledgement. For example, the application can supply its API client
|
|
137
|
+
and error reporter:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
async function uploadRecords(records, signal, token) {
|
|
141
|
+
try {
|
|
142
|
+
await api.uploadLogsWithRetry({ records, token }, { signal });
|
|
143
|
+
} catch (error) {
|
|
144
|
+
reportUploadError(error);
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The application supplies `api.uploadLogsWithRetry` and `reportUploadError`. It must make API resolution mean complete
|
|
151
|
+
acceptance. The backend must deduplicate the batch token in the application scope. A lost response
|
|
152
|
+
or acknowledgement can cause a retry. Docket owns batching, one active callback, scheduling,
|
|
153
|
+
retained batch protection, and acknowledgement. The callback must observe its abort signal.
|
|
154
|
+
Supply a finite request timeout in the callback when required.
|
|
155
|
+
|
|
156
|
+
`onError` is optional. Use it to observe storage or processor failures. If the callback reports
|
|
157
|
+
transport errors itself, avoid reporting them again through this observer. Manual flush rejects
|
|
158
|
+
on failure. A rejected callback retains the batch and pauses automatic attempts. New writes do not
|
|
159
|
+
resume them. Call `flush()` explicitly to resume. Transport backoff stays inside the consumer callback.
|
|
160
|
+
|
|
161
|
+
### Resume a paused upload
|
|
162
|
+
|
|
163
|
+
For background upload, supply `upload.onError` or check `getStatus().paused` in the application's
|
|
164
|
+
status updates. Otherwise a rejected callback can leave upload paused without an application
|
|
165
|
+
notification. Writes continue during a pause. At capacity, the outbox evicts old unprotected
|
|
166
|
+
records or drops incoming records when the protected batch fills capacity. Check `droppedRecords`.
|
|
167
|
+
Resuming does not restore records lost to capacity.
|
|
168
|
+
|
|
169
|
+
Any explicit processor or hub flush resumes upload. This includes `docket.flush()` and a call
|
|
170
|
+
made by `flushOnPageHide(docket)`. Docket installs no page-exit listener itself. To flush only
|
|
171
|
+
local history on page exit, pass `{ flush: () => historyClient.flush() }` to that helper instead.
|
|
172
|
+
Keep offline, call-state, and transport retry gates inside the consumer's send callback. A pending
|
|
173
|
+
callback must observe its abort signal; a rejected callback pauses automatic attempts.
|
|
174
|
+
|
|
175
|
+
The consumer can retry after an explicit user action or a connectivity event. This example uses
|
|
176
|
+
the application's `redactor`, `outbox`, `uploadRecords`, `refreshUploadStatus`, and retry button.
|
|
177
|
+
The status helper updates the interface; the send callback reports transport errors:
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
const docket = createDocket({
|
|
181
|
+
areas: [{ id: 'application' }],
|
|
182
|
+
redactor,
|
|
183
|
+
upload: {
|
|
184
|
+
outbox,
|
|
185
|
+
send: uploadRecords,
|
|
186
|
+
onError: refreshUploadStatus,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
const listenerLifetime = new AbortController();
|
|
190
|
+
|
|
191
|
+
async function resumeUpload() {
|
|
192
|
+
const upload = docket.upload;
|
|
193
|
+
const status = upload?.getStatus();
|
|
194
|
+
if (!status?.paused || status.stopped) return;
|
|
195
|
+
try {
|
|
196
|
+
await upload.flush();
|
|
197
|
+
} catch {
|
|
198
|
+
// The upload observer updates the interface. Keep the batch paused.
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const resume = () => void resumeUpload();
|
|
203
|
+
const listenerOptions = { signal: listenerLifetime.signal };
|
|
204
|
+
retryButton.addEventListener('click', resume, listenerOptions);
|
|
205
|
+
window.addEventListener('online', resume, listenerOptions);
|
|
206
|
+
|
|
207
|
+
async function stopDocket() {
|
|
208
|
+
listenerLifetime.abort();
|
|
209
|
+
await docket.stop();
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The `online` event is a retry trigger, not proof that the backend is available. Another rejection
|
|
214
|
+
keeps the batch paused. The consumer can call the same resume function from its own backoff
|
|
215
|
+
scheduler. Run it after the error observer returns; a synchronous flush inside that observer
|
|
216
|
+
joins the failed operation. Keep resume work bound to the same account. Remove listeners and
|
|
217
|
+
cancel consumer retry timers before account replacement or terminal shutdown.
|
|
218
|
+
|
|
219
|
+
Call `await docket.flush()` for delivery. `docket.stop()` closes admission and cancels upload
|
|
220
|
+
before it flushes and stops history and other sinks. It then stops the outbox, including after
|
|
221
|
+
another cleanup step fails. Stop makes no final request and preserves persistent pending records.
|
|
222
|
+
It is terminal and idempotent. Default memory history remains readable after stop.
|
|
223
|
+
|
|
224
|
+
For logout, first close application admission, for example through `isActive`. Then stop the
|
|
225
|
+
uploader, clear the outbox, stop the composition, and clear history:
|
|
226
|
+
|
|
227
|
+
This flow intentionally discards pending records. If policy requires delivery first, await the
|
|
228
|
+
old account's upload flush before stop, with its original credentials. On failure, retain the
|
|
229
|
+
batch or report an explicit discard. Stop itself makes no final request.
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
const failures = [];
|
|
233
|
+
for (const step of [
|
|
234
|
+
() => persistent.resetLevel(),
|
|
235
|
+
() => persistent.upload.stop(),
|
|
236
|
+
() => persistent.outbox.clear(),
|
|
237
|
+
() => persistent.stop(),
|
|
238
|
+
() => persistent.history.clear(),
|
|
239
|
+
]) {
|
|
240
|
+
try {
|
|
241
|
+
await step();
|
|
242
|
+
} catch (error) {
|
|
243
|
+
failures.push(error);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (failures.length) throw new AggregateError(failures, 'Cleanup failed');
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Hopper outbox clear requires an active adapter. Do not clear it after outbox stop.
|
|
250
|
+
The application owns account changes and credentials. No account controller or automatic disk
|
|
251
|
+
cleanup is added.
|
|
252
|
+
|
|
253
|
+
After a crash or forced sign-out, the next account must use its own resource names. Do not open
|
|
254
|
+
an old account's outbox with a new account's sender. Keep the old names if cleanup needs retry.
|
|
255
|
+
When no old client holds the locks, use Hopper `clearIndexedDbStore(storageOptions.history)` and
|
|
256
|
+
`clearIndexedDbOutbox(storageOptions.outbox)`. Both clear existing storage without creating a
|
|
257
|
+
database. Use the outbox helper for outboxes; history cleanup removes different metadata.
|
|
258
|
+
|
|
259
|
+
## Low-level composition
|
|
260
|
+
|
|
261
|
+
Use `createLogging` when the application already owns its sinks and shutdown order:
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
import { createLogging } from '@bazariodev/docket';
|
|
265
|
+
import { createMemoryHistory } from '@bazariodev/docket/history';
|
|
266
|
+
|
|
267
|
+
const history = createMemoryHistory({ maxRecords: 1000 });
|
|
45
268
|
const logging = createLogging({
|
|
46
269
|
areas: [{ id: 'application' }, { id: 'network', level: 'warn' }],
|
|
47
270
|
redactor,
|
|
48
|
-
sinks: [
|
|
271
|
+
sinks: [history],
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The later examples reuse `conversion`, `redactor`, `history`, and `logging` from these examples.
|
|
276
|
+
Each memory history owns a separate Coordination queue. The application supplies no queue factory.
|
|
277
|
+
|
|
278
|
+
### Console output and custom sinks
|
|
279
|
+
|
|
280
|
+
Console output requires an explicit sink. This example adds it to the hub above:
|
|
281
|
+
|
|
282
|
+
```js
|
|
283
|
+
import { createConsoleSink } from '@bazariodev/docket';
|
|
284
|
+
|
|
285
|
+
const detachConsole = logging.addSink(createConsoleSink({ level: 'info' }));
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
A custom sink receives redacted, frozen records. Its `write(record)` must complete admission
|
|
289
|
+
synchronously. The hub ignores its return value. It does not track returned promises, wait for
|
|
290
|
+
them during flush, or contain their asynchronous rejections. For example:
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
const detachOutput = logging.addSink({
|
|
294
|
+
write(record) {
|
|
295
|
+
output(record);
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The application supplies the synchronous `output` function. Use
|
|
301
|
+
[the upload sink](#periodic-upload) and its `send` callback for backend delivery.
|
|
302
|
+
A custom asynchronous destination must own its bounded buffer and failure handling.
|
|
303
|
+
Its optional `flush()` must wait for work admitted before that call. Its optional `stop()`
|
|
304
|
+
must release its resources. The hub awaits these lifecycle methods.
|
|
305
|
+
|
|
306
|
+
Call `detachOutput()` or `detachConsole()` to stop future delivery to that registration.
|
|
307
|
+
Detach does not flush or stop the sink. The application must complete its cleanup separately.
|
|
308
|
+
|
|
309
|
+
### Page-lifetime loggers and account sinks
|
|
310
|
+
|
|
311
|
+
Keep one root hub when modules cache their loggers for the whole page. Stop is terminal;
|
|
312
|
+
cached loggers from a stopped hub ignore later writes. Creating a new account client cannot
|
|
313
|
+
redirect those loggers. Use the same page hub and replace its account sinks instead:
|
|
314
|
+
|
|
315
|
+
```js
|
|
316
|
+
const logging = createLogging({
|
|
317
|
+
areas: [{ id: 'application' }],
|
|
318
|
+
redactor,
|
|
319
|
+
isActive: () => accountActive,
|
|
49
320
|
});
|
|
50
|
-
const
|
|
51
|
-
|
|
321
|
+
const logger = logging.getLogger('application', 'Connection');
|
|
322
|
+
const detachHistory = logging.addSink(accountHistory);
|
|
323
|
+
const detachUpload = logging.addSink(accountUpload);
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The application supplies the gate and both account sinks. Close admission before an account
|
|
327
|
+
change. Reset levels while the hub is active, detach old sinks, then stop and erase those old
|
|
328
|
+
clients. Attach new sinks before opening admission. Do not call hub stop at account boundaries.
|
|
329
|
+
The hub keeps one `runId` and increasing sequence numbers across accounts. `copyRecords` can
|
|
330
|
+
copy a pre-account source when application policy permits it; attaching a sink copies no backlog.
|
|
331
|
+
|
|
332
|
+
### Level overrides
|
|
333
|
+
|
|
334
|
+
`levelStorage` is optional. Without it, overrides live only in the hub. Supply a distinct key
|
|
335
|
+
for each tenant, account, and hub. For example, use
|
|
336
|
+
`JSON.stringify(['diagnostic-levels', accountStorageKey, hubId])` with the application's storage.
|
|
337
|
+
Do not share one key between hubs or reuse an old account's key for another account.
|
|
338
|
+
Saving replaces that key with this hub's active overrides; it does not merge other areas.
|
|
339
|
+
|
|
340
|
+
Call `resetLevel()` before stop for logout. Reset after stop does nothing. For a page-lifetime
|
|
341
|
+
hub, reset before account replacement, select the new account's storage, then call `reloadLevels()`.
|
|
342
|
+
Use a stable storage port that routes operations to the current account key. Replacing the
|
|
343
|
+
constructor option does not replace the hub's captured port.
|
|
344
|
+
The application owns storage permission, cross-tab notifications, and removal of old keys.
|
|
345
|
+
Expiry uses wall-clock timestamps. A backward clock adjustment can extend elapsed duration;
|
|
346
|
+
`maxOverrideMs` does not promise a monotonic elapsed-time deadline.
|
|
347
|
+
|
|
348
|
+
### Context, Errors, and lazy data
|
|
349
|
+
|
|
350
|
+
Keep scope names stable. Put variable correlation IDs in child context:
|
|
351
|
+
|
|
352
|
+
```js
|
|
353
|
+
const log = logging.getLogger('application', 'Request');
|
|
354
|
+
const requestLog = log.child({ context: { requestId: 'request-123' } });
|
|
355
|
+
requestLog.error('Request failed', new Error('Timed out'));
|
|
356
|
+
requestLog.debug('Request counters', () => readRequestCounters());
|
|
52
357
|
```
|
|
53
358
|
|
|
54
|
-
The
|
|
55
|
-
|
|
56
|
-
|
|
359
|
+
The application supplies the synchronous `readRequestCounters` function. Docket calls it only
|
|
360
|
+
after the initial admission checks. Later state changes or record validation can still discard
|
|
361
|
+
the record. Select a few diagnostic fields; do not return the whole application store.
|
|
362
|
+
The redactor still applies the application's privacy policy to context, Errors, and lazy data.
|
|
57
363
|
|
|
58
364
|
## Consumer redaction policy
|
|
59
365
|
|
|
@@ -86,6 +392,10 @@ another application rule. Custom `policy.text` rules receive the Docket record A
|
|
|
86
392
|
supply a dictionary such as `{ email: TEXT_RULES.email }` as `TEXT_RULES` and omit `policy.text`.
|
|
87
393
|
Unknown fields default to `keep`; use `unknownFields: 'drop'` when only named fields are permitted.
|
|
88
394
|
|
|
395
|
+
Storm summaries also use this field policy. To retain the `logging.suppressed` counts with
|
|
396
|
+
`unknownFields: 'drop'`, allow `scope` and `dropped` with the `keep` action. Otherwise the policy
|
|
397
|
+
can remove those summary fields.
|
|
398
|
+
|
|
89
399
|
Field mask/hash actions convert numbers and bigint to text. Other non-string values use a fixed
|
|
90
400
|
redaction marker without a hash call. Error schema field rules use the same actions. Captured runtime
|
|
91
401
|
accessors supply Error stacks and DOMException fields. Runtime stack formatting can call an
|
|
@@ -120,9 +430,6 @@ An application can also supply its own `Redactor`. The hub still validates, copi
|
|
|
120
430
|
its output within the declared limits. These checks do not prove that a privacy policy removes
|
|
121
431
|
all private data. Select the policy before recording. Export cannot restore removed values.
|
|
122
432
|
|
|
123
|
-
Managed history's account-owner digest is separate from record redaction. It uses native Web
|
|
124
|
-
Crypto. A history redactor can ignore the supplied history key when its policy does not use it.
|
|
125
|
-
|
|
126
433
|
## SIP display names
|
|
127
434
|
|
|
128
435
|
```js
|
|
@@ -159,7 +466,167 @@ The helper limits input and complete output to four times `maxStringLength`. Ove
|
|
|
159
466
|
becomes `[truncated]`. Output growth throws `RangeError`. It counts escaped callback output across
|
|
160
467
|
headers before assembly and stops at the limit. Custom callbacks are trusted application code.
|
|
161
468
|
|
|
162
|
-
##
|
|
469
|
+
## Memory history
|
|
470
|
+
|
|
471
|
+
`createMemoryHistory({ maxRecords })` keeps the newest records in page memory.
|
|
472
|
+
`maxRecords` must be a positive safe integer. It has no default. The only configuration key is
|
|
473
|
+
`maxRecords`; unsupported keys reject instead of silently ignoring retention options.
|
|
474
|
+
At capacity, each write removes the oldest entry before it adds the
|
|
475
|
+
new record. Repeated record IDs remain separate entries. The sink keeps references to the
|
|
476
|
+
redacted, frozen records from Docket core; it does not copy their payloads.
|
|
477
|
+
|
|
478
|
+
`query({ areas?, from?, to? })` captures a snapshot during the call. Bounds are inclusive;
|
|
479
|
+
empty areas select nothing. Results use admission order, including when timestamps move
|
|
480
|
+
backwards. Each query returns a separate array. Later writes and clear do not change it.
|
|
481
|
+
Changing that array does not change history or another snapshot.
|
|
482
|
+
`clear()` immediately releases retained records and permits later writes. It does not change
|
|
483
|
+
logger levels or the upload sink. History has no timer, flush, or stop method. Records remain
|
|
484
|
+
available after the logging hub stops, until clear or page exit.
|
|
485
|
+
|
|
486
|
+
History and upload accept valid immutable `LogRecord` values. They do not validate raw logger input.
|
|
487
|
+
Use Docket core to create, redact, validate, and freeze records before delivery.
|
|
488
|
+
|
|
489
|
+
The default client history keeps the last 1,000 records. It has no duration guarantee or separate
|
|
490
|
+
area budget. At five records per second, 1,000 records cover about 200 seconds. With the default
|
|
491
|
+
`maxRecordSize` of 65,536 characters, retained serialized records can total 65,536,000 characters.
|
|
492
|
+
This is not a heap-byte limit. Copies, snapshots, and archives use additional memory.
|
|
493
|
+
|
|
494
|
+
For separate area budgets, size limits, or age retention, inject a Hopper store:
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
import type { LogRecord } from '@bazariodev/docket';
|
|
498
|
+
import { createMemoryStore } from '@bazariodev/hopper';
|
|
499
|
+
|
|
500
|
+
const retainedHistory = createMemoryStore<LogRecord>({
|
|
501
|
+
maxAgeMs: 3_600_000,
|
|
502
|
+
maxTotalSize: 16_000_000,
|
|
503
|
+
defaultMaxSizePerArea: 1_000_000,
|
|
504
|
+
maxSizePerArea: { application: 4_000_000 },
|
|
505
|
+
maxRecordsPerArea: { application: 20_000 },
|
|
506
|
+
});
|
|
507
|
+
const withAreaBudgets = createDocket({
|
|
508
|
+
areas: [{ id: 'application' }],
|
|
509
|
+
redactor,
|
|
510
|
+
history: retainedHistory,
|
|
511
|
+
});
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
The application supplies `redactor`. Hopper's IndexedDB history has the same area budgets and
|
|
515
|
+
query filters. It already stores an `area` field and maintains an area/time index. Hopper accepts
|
|
516
|
+
an omitted area for a single stream and uses `default`. Docket records retain their configured areas.
|
|
517
|
+
An age limit permits expiry; it does not guarantee that size or count limits retain a full hour.
|
|
518
|
+
|
|
519
|
+
### Capacity and input cost
|
|
520
|
+
|
|
521
|
+
- `maxRecords` limits record count, not heap bytes. History and outbox have separate capacities
|
|
522
|
+
and can retain separate copies. Serialization limits do not give an exact heap-size limit.
|
|
523
|
+
- Persistent storage also uses RAM for records, queues, and transaction work.
|
|
524
|
+
- Avoid whole application stores, responses, and large dictionaries as log data. Select a few
|
|
525
|
+
fields at the call site. The field limit does not prevent enumeration of all input keys.
|
|
526
|
+
- Lazy data avoids work when initial admission checks reject a call. An input that passes these
|
|
527
|
+
checks can still be expensive or fail later validation.
|
|
528
|
+
- Use stable scope names. Put variable correlation IDs in context to avoid extra root loggers.
|
|
529
|
+
- Release query results when no longer needed. Old snapshots retain their records after clear.
|
|
530
|
+
- Stop the lifecycle owner and call helper disposers. Client stop does not erase history.
|
|
531
|
+
|
|
532
|
+
## Periodic upload
|
|
533
|
+
|
|
534
|
+
The application supplies `sendRecords` and `reportUploadError` in this example.
|
|
535
|
+
`sendRecords(records, signal, token)` must accept the complete batch before its promise resolves.
|
|
536
|
+
It owns the endpoint, account credentials, encoding, response checks, and request timeout.
|
|
537
|
+
Use the token as an idempotency key in the account and application scope.
|
|
538
|
+
|
|
539
|
+
```js
|
|
540
|
+
import { createUploadSink } from '@bazariodev/docket/upload';
|
|
541
|
+
import { createMemoryOutbox } from '@bazariodev/hopper/outbox';
|
|
542
|
+
|
|
543
|
+
const outbox = createMemoryOutbox({ maxRecords: 1000 });
|
|
544
|
+
const upload = createUploadSink({
|
|
545
|
+
maxBatchRecords: 100,
|
|
546
|
+
intervalMs: 15_000,
|
|
547
|
+
send: sendRecords,
|
|
548
|
+
onError: reportUploadError,
|
|
549
|
+
}, outbox);
|
|
550
|
+
const detachUpload = logging.addSink(upload);
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
The upload sink borrows the outbox. The application owns storage capacity, erasure, and shutdown.
|
|
554
|
+
Use one upload processor per outbox. Each write returns synchronously and starts no request.
|
|
555
|
+
One timer starts an automatic flush after `intervalMs`. Existing pending records also start
|
|
556
|
+
this timer when the processor is created. An idle sink has no timer. Browser suspension can
|
|
557
|
+
delay an attempt. The processor installs no page-exit send. An application-installed page-exit
|
|
558
|
+
helper can call the hub flush and start or resume upload; select local history alone when needed.
|
|
559
|
+
|
|
560
|
+
- `maxBatchRecords`, `intervalMs`, `send`, and the outbox are required. `onError` is optional.
|
|
561
|
+
A supplied observer must be a function.
|
|
562
|
+
- Batch limit and interval must be positive safe integers. The interval cannot exceed
|
|
563
|
+
2,147,483,647 ms. Neither option has a default.
|
|
564
|
+
- A supplied scheduler needs `setTimeout` and `clearTimeout`. The default uses host timers.
|
|
565
|
+
- The outbox port needs `write`, `capture`, `flush`, `nextBatch`, `acknowledge`, and `getStatus`.
|
|
566
|
+
Docket has no runtime dependency on Hopper. Another adapter can implement this port.
|
|
567
|
+
- Only one send runs at a time. `send` receives a frozen record array, an abort signal,
|
|
568
|
+
and the protected batch token. Resolve only after complete backend acceptance.
|
|
569
|
+
- A failed send or acknowledgement keeps the same logical batch and token for retry.
|
|
570
|
+
Hopper reuses the array within one instance. A recovered batch can exceed a new batch limit.
|
|
571
|
+
After send resolves, the same processor retries a failed acknowledgement without another send.
|
|
572
|
+
A new processor has no such acceptance state and can send that protected token again.
|
|
573
|
+
- Hopper outboxes copy JSON records, preserve FIFO order and duplicate admissions, and protect
|
|
574
|
+
one batch. Overflow removes the oldest unprotected record. If the protected batch fills
|
|
575
|
+
capacity, the incoming record is discarded. Read `droppedRecords` for these losses.
|
|
576
|
+
- `getStatus()` includes the adapter mode, error, record counts, and processor `paused` and `stopped` states.
|
|
577
|
+
A failed or stopped adapter starts no automatic retry. Inspect the error and retry explicitly.
|
|
578
|
+
- A rejected callback pauses automatic attempts. The consumer owns retries inside `send`.
|
|
579
|
+
An explicit `flush()` resumes the retained batch. New writes alone do not resume it.
|
|
580
|
+
- A failed adapter rejects admission. The processor counts each refused record as dropped and
|
|
581
|
+
reports the storage failure once per observed failure episode. Local history keeps recording.
|
|
582
|
+
Processor drops are local to that processor; adapter overflow counts remain in adapter status.
|
|
583
|
+
|
|
584
|
+
Manual `flush()` cancels the timer. It captures the admission boundary before asynchronous work.
|
|
585
|
+
Writes alone do not extend it. A concurrent flush joins the same promise and extends the boundary.
|
|
586
|
+
The first failure rejects that flush. A supplied `onError` observes the failure once. The reporter is not
|
|
587
|
+
awaited; its throws and rejected promises are contained. A flush called synchronously by
|
|
588
|
+
`onError` joins the failed operation. Do not await a flush from a callback that the flush needs.
|
|
589
|
+
Adapter and processor observers report separate operations. Select one reporting path or deduplicate
|
|
590
|
+
them in the application when the same failure reaches both. Repeated explicit retries can report
|
|
591
|
+
new attempt failures. Keep a status observer or status checks active when using background upload.
|
|
592
|
+
|
|
593
|
+
For persistent pending records, replace only the adapter:
|
|
594
|
+
|
|
595
|
+
```js
|
|
596
|
+
import { createIndexedDbOutbox } from '@bazariodev/hopper/outbox/indexeddb';
|
|
597
|
+
|
|
598
|
+
const outbox = await createIndexedDbOutbox({
|
|
599
|
+
...storageOptions.outbox,
|
|
600
|
+
maxRecords: 1000,
|
|
601
|
+
onError: reportUploadError,
|
|
602
|
+
});
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
The asynchronous factory restores records and their protected batch before it returns.
|
|
606
|
+
Supply required `databaseName` and `lockName` values in the initial options. The adapter uses
|
|
607
|
+
the supplied lock name unchanged. All writers of one database must use that same lock name.
|
|
608
|
+
Missing or invalid names reject before platform access.
|
|
609
|
+
It requires IndexedDB and exclusive Web Locks ownership. It rejects unavailable storage,
|
|
610
|
+
ownership, and incompatible schemas. It does not silently switch to memory.
|
|
611
|
+
Use a separate database for download history. No database is accessed by the memory adapter.
|
|
612
|
+
|
|
613
|
+
Outbox `flush()` means storage completion. Upload `flush()` also waits for backend acceptance
|
|
614
|
+
and local acknowledgement. Delivery is at least once. A lost response or a new processor after
|
|
615
|
+
failed acknowledgement can cause a duplicate send. Storage removal, overflow, and page exit before storage commit can
|
|
616
|
+
still lose records. No adapter provides exactly-once delivery.
|
|
617
|
+
|
|
618
|
+
Batch limits count records, not encoded request bytes. The client's default 100-record batch can contain
|
|
619
|
+
6,553,600 serialized characters before request framing. Choose a smaller `maxBatchRecords` for
|
|
620
|
+
large records. The consumer can split one protected batch into bounded requests; resolve only
|
|
621
|
+
after every part is accepted. Use a stable, distinct deduplication key for each part.
|
|
622
|
+
|
|
623
|
+
A permanent rejection must not silently become acceptance. In particular, do not treat every
|
|
624
|
+
4xx response as success. The consumer can stop the processor, inspect the protected batch, report
|
|
625
|
+
an intentional discard, then acknowledge that exact token. `outbox.clear()` discards all pending
|
|
626
|
+
records. Both are application data-loss decisions. Authentication failures must not switch a batch
|
|
627
|
+
to another account's credentials.
|
|
628
|
+
|
|
629
|
+
## Export and download
|
|
163
630
|
|
|
164
631
|
```js
|
|
165
632
|
import { createLogBundle } from '@bazariodev/docket/export';
|
|
@@ -167,59 +634,177 @@ import { downloadBlob } from '@bazariodev/docket/browser';
|
|
|
167
634
|
import * as archive from '@bazariodev/baler';
|
|
168
635
|
|
|
169
636
|
const bundle = await createLogBundle({
|
|
170
|
-
source:
|
|
637
|
+
source: history,
|
|
171
638
|
redactor,
|
|
172
639
|
files: [{ name: 'application.log', areas: ['application', 'network'] }],
|
|
173
640
|
}, archive);
|
|
174
641
|
downloadBlob(bundle.blob, bundle.fileName);
|
|
175
642
|
```
|
|
176
643
|
|
|
177
|
-
The
|
|
178
|
-
|
|
179
|
-
|
|
644
|
+
The bundle uses one fixed snapshot while recording continues. Download neither drains history
|
|
645
|
+
nor acknowledges upload records. It works without an uploader and during upload failure.
|
|
646
|
+
The manifest uses the minimum and maximum selected timestamps for its actual range. Returned
|
|
647
|
+
snapshots and files belong to their caller. Clear cannot erase those returned values.
|
|
180
648
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
649
|
+
`maxInputSize` defaults to 32,000,000 UTF-8 bytes across all files and the manifest.
|
|
650
|
+
The exporter checks each formatted record, including its trailing newline, before retaining it.
|
|
651
|
+
It checks custom output on return and checks the serialized manifest before archive assembly.
|
|
652
|
+
Overflow rejects with `RangeError` and stops later rendering and archive work. The archive
|
|
653
|
+
provider also checks the final input size.
|
|
186
654
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
655
|
+
This limit bounds retained export text, not peak heap use. The source can allocate its snapshot
|
|
656
|
+
before formatting. One current record, a custom renderer, or manifest serialization can allocate
|
|
657
|
+
before its size check. Synchronous custom rendering cannot be interrupted.
|
|
658
|
+
|
|
659
|
+
### JSON and NDJSON
|
|
660
|
+
|
|
661
|
+
Use queried records directly for structured output. They have already passed redaction:
|
|
662
|
+
|
|
663
|
+
```js
|
|
664
|
+
const records = await history.query();
|
|
665
|
+
const json = JSON.stringify(records);
|
|
666
|
+
const ndjson = records.map((record) => `${JSON.stringify(record)}\n`).join('');
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
These application-owned strings have no bundle byte limit. Bound the selected history and
|
|
670
|
+
release snapshots and strings when no longer needed. Do not add raw input after redaction.
|
|
671
|
+
|
|
672
|
+
## Shutdown and account changes
|
|
673
|
+
|
|
674
|
+
`createDocket().flush()` waits for its sinks, including delivery through a configured uploader.
|
|
675
|
+
Its `stop()` cancels upload before sink cleanup and starts no final send. Flush explicitly
|
|
676
|
+
before stop when application policy requires delivery. Rejection pauses automatic upload;
|
|
677
|
+
another explicit flush resumes it. The consumer owns request retry and timeout policy.
|
|
678
|
+
|
|
679
|
+
Browser helpers have separate disposers. This example reuses the memory client from Usage:
|
|
680
|
+
|
|
681
|
+
```js
|
|
682
|
+
import { captureGlobalErrors } from '@bazariodev/docket/browser';
|
|
683
|
+
|
|
684
|
+
const disposeErrors = captureGlobalErrors(
|
|
685
|
+
docket.getLogger('application', 'Global'),
|
|
686
|
+
);
|
|
687
|
+
try {
|
|
688
|
+
await docket.flush();
|
|
689
|
+
} finally {
|
|
690
|
+
disposeErrors();
|
|
691
|
+
await docket.stop();
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Stop preserves history. Use the [account cleanup sequence below](#account-cleanup)
|
|
696
|
+
when application policy requires erasure.
|
|
697
|
+
|
|
698
|
+
`logging.stop()` closes admission, flushes each sink, then stops each sink. The uploader's flush
|
|
699
|
+
includes retained work when it joins an active operation. A failure still runs sink cleanup and
|
|
700
|
+
reaches the hub's aggregate error. History remains available for download.
|
|
701
|
+
|
|
702
|
+
### Account cleanup
|
|
703
|
+
|
|
704
|
+
For logout, stop sending before the hub can flush the uploader. Then erase and close storage:
|
|
705
|
+
|
|
706
|
+
```js
|
|
707
|
+
const failures = [];
|
|
708
|
+
for (const step of [
|
|
709
|
+
() => logging.resetLevel(),
|
|
710
|
+
() => upload.stop(),
|
|
711
|
+
() => outbox.clear(),
|
|
712
|
+
() => logging.stop(),
|
|
713
|
+
() => outbox.stop(),
|
|
714
|
+
() => history.clear(),
|
|
715
|
+
]) {
|
|
716
|
+
try {
|
|
717
|
+
await step();
|
|
718
|
+
} catch (error) {
|
|
719
|
+
failures.push(error);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (failures.length) throw new AggregateError(failures, 'Cleanup failed');
|
|
200
723
|
```
|
|
201
724
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
`
|
|
206
|
-
|
|
725
|
+
For shutdown that preserves pending records, stop the uploader, then stop the outbox.
|
|
726
|
+
Upload stop cancels its timer, signals the active send, and starts no final send or retry.
|
|
727
|
+
It resolves without waiting for an uncooperative send. An interrupted flush rejects with
|
|
728
|
+
`AbortError`. Late send settlement cannot start an acknowledgement or restart processing.
|
|
729
|
+
An acknowledgement started before stop can still complete. Store clear and stop wait for
|
|
730
|
+
storage work. Upload stop neither clears nor closes the borrowed adapter.
|
|
731
|
+
Writes after processor stop are ignored. Processor flush after stop resolves without sending.
|
|
732
|
+
|
|
733
|
+
`detachUpload()` prevents future hub delivery. The application must still stop the uploader.
|
|
734
|
+
The application owns account changes, credentials, logger-level resets, and cancellation of
|
|
735
|
+
outstanding downloads. Bind each uploader to its original account; stop it before credentials
|
|
736
|
+
change, then create a new uploader. Local clear does not stop or flush upload.
|
|
737
|
+
|
|
738
|
+
`createLogHistory` and its account/session types are removed. The replacement has no account
|
|
739
|
+
controller or automatic legacy cleanup. The memory helper never accesses disk history.
|
|
740
|
+
Deployed consumers need a separate migration for legacy storage, including prior memory mode.
|
|
741
|
+
Switching to memory does not erase that storage. Standalone `/indexeddb` remains available.
|
|
742
|
+
|
|
743
|
+
TypeScript imports from the former `/history` entry also change:
|
|
744
|
+
|
|
745
|
+
| Former type | Current entry |
|
|
746
|
+
| --- | --- |
|
|
747
|
+
| `CopyRecordsOptions` | `@bazariodev/hopper` |
|
|
748
|
+
| `LogOverflowStrategy`, `LogStoreError`, `MemoryLogStoreOptions` | `@bazariodev/docket/indexeddb` |
|
|
749
|
+
|
|
750
|
+
Use the new `/history` types only for FIFO memory history. Hopper exposes its generic store
|
|
751
|
+
types directly; Docket's `/indexeddb` keeps its logging-specific compatibility types.
|
|
752
|
+
|
|
753
|
+
### Migrate storage from 0.0.0
|
|
754
|
+
|
|
755
|
+
The old managed history could create an empty database and owner metadata during account
|
|
756
|
+
transitions, even in memory mode. Use the original database override if one was supplied.
|
|
757
|
+
Otherwise its names were:
|
|
758
|
+
|
|
759
|
+
| Resource | Legacy name |
|
|
760
|
+
| --- | --- |
|
|
761
|
+
| Managed database | `bazario-logging:history:${leaseKey}` |
|
|
762
|
+
| Managed localStorage metadata | `bazario-logging:metadata:${databaseName}` |
|
|
763
|
+
| Managed or standalone database lock | `JSON.stringify(['bazario:logging:indexeddb', databaseName])` |
|
|
764
|
+
| Standalone database without an override | `bazario-logging` |
|
|
765
|
+
|
|
766
|
+
The metadata can contain a salt, full owner digest, level overrides, and a persistent-mode key.
|
|
767
|
+
Remove it as well as the database when that legacy history is no longer needed. Stop old clients
|
|
768
|
+
first. A new Hopper default lock does not exclude a live 0.0.0 writer; supply the exact legacy lock.
|
|
769
|
+
Keep this lock protocol until all writers of that database have migrated. Use separate names
|
|
770
|
+
for a new account schema.
|
|
771
|
+
|
|
772
|
+
The application owns permanent erasure of its dedicated database. Acquire its exact exclusive
|
|
773
|
+
lock with `ifAvailable: true`; refuse when it is held. Under that lock, call
|
|
774
|
+
`indexedDB.deleteDatabase(databaseName)` and await native success or error. After success, call
|
|
775
|
+
`localStorage.removeItem(metadataKey)` for the managed history key. Do not remove unrelated
|
|
776
|
+
origin data. Deletion creates no database and needs no compatible schema or metadata writes.
|
|
777
|
+
The same path can erase a corrupt dedicated outbox when schema-preserving clear cannot proceed.
|
|
778
|
+
|
|
779
|
+
Native deletion cannot be canceled. If another connection blocks it, close old tabs or connections
|
|
780
|
+
and keep the lock until deletion settles. A timeout must not release ownership while deletion
|
|
781
|
+
remains pending. Never report completed erasure before native success. See
|
|
782
|
+
[MDN deleteDatabase](https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/deleteDatabase).
|
|
783
|
+
The repository's consumer example supplies `eraseDiagnosticStorage` for this operation;
|
|
784
|
+
it is application wiring, not a Docket export.
|
|
207
785
|
|
|
208
786
|
## Persistent storage
|
|
209
787
|
|
|
788
|
+
Use a Hopper store directly as a sink and an export source. Both Hopper history adapters
|
|
789
|
+
sort by timestamp, run ID, and sequence. They keep the first retained `(runId, seq)` record.
|
|
790
|
+
This differs from the FIFO helper, which preserves duplicate admissions.
|
|
791
|
+
|
|
210
792
|
```js
|
|
211
|
-
import { createIndexedDbLogStore } from '@bazariodev/docket/indexeddb';
|
|
212
793
|
import { createIndexedDbStore } from '@bazariodev/hopper/indexeddb';
|
|
213
794
|
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
);
|
|
218
|
-
//
|
|
219
|
-
await
|
|
220
|
-
const status =
|
|
795
|
+
const history = createIndexedDbStore({
|
|
796
|
+
...storageOptions.history,
|
|
797
|
+
});
|
|
798
|
+
const detachHistory = logging.addSink(history);
|
|
799
|
+
// Pass history as the source for createLogBundle.
|
|
800
|
+
await history.flush();
|
|
801
|
+
const status = history.getStatus();
|
|
221
802
|
```
|
|
222
803
|
|
|
804
|
+
`/indexeddb` is an optional structural adapter. Supply Hopper's factory as its second argument.
|
|
805
|
+
Both `databaseName` and `lockName` are required. The adapter forwards them unchanged and rejects
|
|
806
|
+
missing or invalid names before calling the provider. It supplies no database or lock default.
|
|
807
|
+
|
|
223
808
|
The supplied Hopper store first tries an exclusive database lock. A blocked or unavailable lock selects memory
|
|
224
809
|
fallback for that instance's lifetime. It cannot later replay that fallback into another owner's
|
|
225
810
|
history. A new instance can try ownership again. With `allowUnprotected: true`, failed acquisition
|
|
@@ -233,99 +818,19 @@ An open or write failure with permitted access starts bounded memory fallback. R
|
|
|
233
818
|
every ten minutes. A failed write batch is lost and its flush rejects. Other pending records move
|
|
234
819
|
to fallback. Successful recovery removes copied fallback records only after commit.
|
|
235
820
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
```js
|
|
242
|
-
import { createBaton } from '@bazariodev/baton';
|
|
243
|
-
import { createRedactor } from '@bazariodev/docket/redaction';
|
|
244
|
-
import { createLogHistory } from '@bazariodev/docket/history';
|
|
245
|
-
import { createIndexedDbLogStore } from '@bazariodev/docket/indexeddb';
|
|
246
|
-
import { copyRecords, createMemoryStore, validateStoreOptions } from '@bazariodev/hopper';
|
|
247
|
-
import { createIndexedDbStore } from '@bazariodev/hopper/indexeddb';
|
|
248
|
-
|
|
249
|
-
let history;
|
|
250
|
-
const baton = createBaton({
|
|
251
|
-
key: JSON.stringify(['example-app', 'production', 'example-tenant']),
|
|
252
|
-
async beforeRelease() { await history?.stop(); },
|
|
253
|
-
});
|
|
254
|
-
const claim = await baton.take();
|
|
255
|
-
if (claim.state !== 'owner') throw new Error('Application ownership is unavailable');
|
|
256
|
-
history = createLogHistory({
|
|
257
|
-
lease: claim.lease,
|
|
258
|
-
logging: { areas: [{ id: 'application' }] },
|
|
259
|
-
createRedactor: () => createRedactor({ fields: { password: 'drop' } }, conversion),
|
|
260
|
-
stores: {
|
|
261
|
-
createMemoryStore,
|
|
262
|
-
createIndexedDbStore: (options) => createIndexedDbLogStore(options, createIndexedDbStore),
|
|
263
|
-
validateStoreOptions,
|
|
264
|
-
copyRecords,
|
|
265
|
-
},
|
|
266
|
-
});
|
|
267
|
-
const log = history.getLogger('application', 'Connection');
|
|
268
|
-
log.info('Application startup');
|
|
269
|
-
await history.setOwner('example-tenant:example-account');
|
|
270
|
-
log.info('Account ready');
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
History owns each supplied store and its private serial queue. It stops each store.
|
|
274
|
-
The `stores` object groups the two factories, `validateStoreOptions`, and `copyRecords`.
|
|
275
|
-
Managed hubs create their own run IDs and admission guard. `logging.createRunId` and
|
|
276
|
-
`logging.isActive` are rejected. The helper must control record identity and session validity.
|
|
277
|
-
Forwarding child loggers capture scope and context when created. Later changes to the supplied
|
|
278
|
-
binding do not change that snapshot. Invalid bindings remain contained.
|
|
279
|
-
The persistent factory must support cleanup even in memory or off mode. A prior account can
|
|
280
|
-
have records on disk. Missing cleanup must not be reported as successful erasure. For standalone
|
|
281
|
-
memory logging without account-history coordination, use the root with a memory sink.
|
|
282
|
-
|
|
283
|
-
The helper generates history keys internally. The redactor factory can ignore the key, or pass
|
|
284
|
-
it to an application-selected synchronous hasher. Use `resume(ownerId)` after reload with a verified
|
|
285
|
-
current account. Use `setOwner(ownerId)` for explicit sign-in or account replacement.
|
|
286
|
-
|
|
287
|
-
| Application event | Helper call | Application work |
|
|
288
|
-
| --- | --- | --- |
|
|
289
|
-
| Startup after lease acquisition | Create history; keep `history.getLogger(area, scope)` | Supply recording policy before producers start. |
|
|
290
|
-
| Export before sign-in | `getSession()`; use its source and redactor | Startup recording needs no account ID. |
|
|
291
|
-
| Reload with a verified account | `resume(ownerId)` | Supply the current authenticated identity. |
|
|
292
|
-
| Explicit sign-in or account replacement | `setOwner(ownerId)` | Wait for authentication first. |
|
|
293
|
-
| Manual logout | `logout()` | Show cleanup failure; do not claim erasure until success. |
|
|
294
|
-
| Forced authentication failure | Keep the helper | Retain history and key; stop only if recording must stop. |
|
|
295
|
-
| Clear history | `clear()` | Refresh status in the view. |
|
|
296
|
-
| Recording or persistence setting | `setMode(mode)` | Supply the permitted mode. |
|
|
297
|
-
| Failed deletion | `retryCleanup()` | Show the resulting status. |
|
|
298
|
-
| Environment or tenant change | Stop the old baton; create a new baton and helper | Await release before acquisition. |
|
|
299
|
-
| Teardown | `stop()` | Stop application producers. |
|
|
300
|
-
| Intentional navigation | Await `baton.pass()` | Its release callback stops producers and history. |
|
|
301
|
-
|
|
302
|
-
Keep helper loggers in page-lifetime modules. They select the current session on each call.
|
|
303
|
-
Session loggers and sources remain bound to one session. After replacement, old logger writes do
|
|
304
|
-
nothing; old sources, redactors, and controls fail with `stale`. `onChange` refreshes export views
|
|
305
|
-
and producers that need session-bound handles. Stop and lease loss permanently disable the helper.
|
|
306
|
-
|
|
307
|
-
Startup recording uses memory only, with 250,000 JSON characters per area and 1,000,000 total by
|
|
308
|
-
default. `startup: false` disables it. Saved off mode also disables it. Its records transfer once
|
|
309
|
-
into the first owner session. Transfer uses stored redacted records and preserves their identities
|
|
310
|
-
and policy tags. Clear, logout, off, and stop discard the startup buffer.
|
|
311
|
-
|
|
312
|
-
The default database name is `bazario-logging:history:` followed by the application lease key.
|
|
313
|
-
The helper keeps one localStorage metadata record for that database. It contains the application
|
|
314
|
-
key, a random salt and full SHA-256 owner digest, mode, revision, cleanup marker, and level
|
|
315
|
-
overrides. Raw owner IDs stay in memory. A key is saved only for clean persistent history.
|
|
316
|
-
Startup, memory-only, and temporary cleanup-failure keys stay in the page. A memory reload uses
|
|
317
|
-
a new key. Promotion to persistence saves its key only after copying commits.
|
|
318
|
-
|
|
319
|
-
Destructive changes save pending cleanup before deletion. A failed clear keeps that marker and
|
|
320
|
-
uses a fresh, isolated memory session. Successful retry discards that temporary history and rotates
|
|
321
|
-
its key. Metadata failure blocks recording. Managed history rejects unprotected persistence.
|
|
322
|
-
Never attach a raw store beside its managed store. Pass `history.clear` to developer controls.
|
|
323
|
-
Cancel application work that holds snapshots when the account changes; returned data cannot be recalled.
|
|
821
|
+
`clearIndexedDbLogStore(options, clearStore)` supplies the same logging database and lock names
|
|
822
|
+
to a standalone cleanup provider. With Hopper `clearIndexedDbStore`, it clears existing records
|
|
823
|
+
and metadata without creating a database. Listing can still access IndexedDB. The new memory
|
|
824
|
+
entries never call this cleanup path.
|
|
324
825
|
|
|
325
826
|
## Behavior
|
|
326
827
|
|
|
327
828
|
`isActive` is an optional admission guard. Returning `false` stops a record before conversion or
|
|
328
|
-
delivery. The hub checks it again after application callbacks.
|
|
829
|
+
delivery. The hub checks it again after application callbacks. `createDocket` captures this
|
|
830
|
+
function from the original options, including prototype methods, and calls it with that options object as
|
|
831
|
+
its receiver. State changes on the original object take effect on later checks. A replacement
|
|
832
|
+
method does not replace the captured gate. If the gate stops the client, the current record is
|
|
833
|
+
blocked before delivery. The application supplies this guard when it needs one.
|
|
329
834
|
|
|
330
835
|
- Levels are `debug`, `info`, `warn`, and `error`. Default area level is `info`.
|
|
331
836
|
- Data functions run only after admission. No sinks, stop, levels, and storm limits can prevent admission.
|
|
@@ -342,7 +847,8 @@ delivery. The hub checks it again after application callbacks. Managed history s
|
|
|
342
847
|
- Default storm allowance is 100 records per area/scope each second. One timer reports suppression.
|
|
343
848
|
- Sink failures do not stop other sinks. Recursive logging from sinks and reporters is rejected.
|
|
344
849
|
|
|
345
|
-
The store defaults and recovery rules below describe the Hopper
|
|
850
|
+
The standalone store defaults and recovery rules below describe the Hopper provider.
|
|
851
|
+
They do not apply to the FIFO history helper or outbox adapters.
|
|
346
852
|
Other providers must state their supported behavior.
|
|
347
853
|
|
|
348
854
|
- Queries use inclusive time bounds and order by time, run ID, then sequence. Empty areas select nothing.
|
|
@@ -361,9 +867,11 @@ Other providers must state their supported behavior.
|
|
|
361
867
|
| Entry | Exports |
|
|
362
868
|
| --- | --- |
|
|
363
869
|
| Root | `createLogging`, `createConsoleSink`, `formatRecord` |
|
|
364
|
-
| `/
|
|
365
|
-
| `/
|
|
366
|
-
| `/
|
|
870
|
+
| `/client` | `createDocket(options)` |
|
|
871
|
+
| `/redaction` | `createRedactor(policy, providers)` |
|
|
872
|
+
| `/indexeddb` | `createIndexedDbLogStore(options, createStore)`, `clearIndexedDbLogStore(options, clearStore)` |
|
|
873
|
+
| `/history` | `createMemoryHistory(options)` |
|
|
874
|
+
| `/upload` | `createUploadSink(options, outbox)` |
|
|
367
875
|
| `/export` | `createLogBundle(options, archive)` |
|
|
368
876
|
| `/browser` | `captureGlobalErrors`, `flushOnPageHide`, `collectBrowserEnvironment`, `downloadBlob` |
|
|
369
877
|
| `/sip` | `createSipTools(textRules)`, `describeSipMessage`. The tools expose `redactSipMessage`, `describeSipMessage`, and `SDP_TEXT_RULES`. |
|
|
@@ -371,12 +879,14 @@ Other providers must state their supported behavior.
|
|
|
371
879
|
|
|
372
880
|
| Component | Options and methods |
|
|
373
881
|
| --- | --- |
|
|
882
|
+
| Docket client | Hub options plus `history` and `upload`. Returns hub methods, `history`, `outbox`, and `upload`. Disabled features are `undefined`. Stop cancels sending before storage shutdown. |
|
|
374
883
|
| Hub | Required `areas`, `redactor`; optional `sinks`, `context`, `levelStorage`, `maxOverrideMs`, `stormLimit`, `scheduler`, clocks, `createRunId`, `onError`, `isActive`. Methods: `getLogger`, `setContext`, `setLevel`, `resetLevel`, `reloadLevels`, `getLevels`, `addSink`, `flush`, `stop`. |
|
|
375
884
|
| Logger | `debug`, `info`, `warn`, `error` accept `(message, data?)`. `child({ scope?, context? })` adds bindings. |
|
|
376
885
|
| Redactor | Policy: `version`, `hash`, `fields`, `unknownFields`, `text`, `limits`. Methods: `redact`, `redactValue`, `redactText`, `mask`, optional `hash`. Resolved `limits` and `version` are public. |
|
|
377
886
|
| Memory store | `maxSizePerArea`, `defaultMaxSizePerArea`, `maxTotalSize`, `maxAgeMs`, `now`, `overflowStrategy`, `onRotate`, `scheduler`, `onError`. Methods: `write`, `query`, `flush`, `clear`, `stop`, `getStatus`. |
|
|
378
|
-
| IndexedDB store | Memory options plus `databaseName`, `indexedDB`, `locks`, `allowUnprotected`, batch/queue/interval/count limits, and `fallback`. |
|
|
379
|
-
|
|
|
887
|
+
| IndexedDB store | Memory options plus required `databaseName` and `lockName`, optional `indexedDB`, `locks`, `allowUnprotected`, batch/queue/interval/count limits, and `fallback`. |
|
|
888
|
+
| Memory history | Required `maxRecords`. Queue creation is internal. Methods: `write`, `query`, `clear`. |
|
|
889
|
+
| Upload sink | Required `maxBatchRecords`, `intervalMs`, `send`, and an outbox. Optional `onError` and `scheduler`. Methods: `write`, `flush`, `stop`, `getStatus`. |
|
|
380
890
|
| Bundle | Required `source`, `redactor`, `files`, and the separate archive capability. Optional time bounds, `manifest`, `fileName`, `maxInputSize`, clock, scheduler. Returns `blob`, `fileName`, file counts. |
|
|
381
891
|
| Supplied Hopper copy helper | `copyRecords(from, to, { maxChunkRecords?, maxChunkSize? })`; defaults are 100 records and 1,000,000 JSON characters. It flushes between chunks. |
|
|
382
892
|
| Console sink | Optional minimum `level` and injected `console`. |
|
|
@@ -397,20 +907,45 @@ Bundle filenames cannot contain paths, reserved device names, or manifest varian
|
|
|
397
907
|
uses NFC and ignores case. Empty files are omitted; the manifest retains their zero counts.
|
|
398
908
|
File names, each file's area list, and the download name are checked before the source query. Its default timestamp is
|
|
399
909
|
the call's start time. The manifest build time is recorded after rendering.
|
|
400
|
-
Custom renderers are synchronous. Built-in formatting yields during large files.
|
|
401
|
-
|
|
402
|
-
rendering and before compression. It does not bound renderer allocations or peak heap use.
|
|
910
|
+
Custom renderers are synchronous. Built-in formatting yields during large files. The
|
|
911
|
+
[export byte limit](#export-and-download) applies during formatting and before compression.
|
|
403
912
|
The supplied Baler implementation uses raw deflate when available and stored entries otherwise.
|
|
404
913
|
The manifest contains ranges, counts, truncation, warnings, policy versions, store status, and
|
|
405
914
|
redacted application metadata. It contains no history key.
|
|
406
915
|
|
|
916
|
+
`formatRecord` produces human-readable text with flat labels. Context and field keys can repeat
|
|
917
|
+
header names such as `runId` or `message`. Choose distinct data names and use structured records
|
|
918
|
+
for machine parsing; the text format does not guarantee unique keys across those sections.
|
|
919
|
+
|
|
407
920
|
Browser listeners are explicit and return cleanup. Environment defaults are language, UTC offset,
|
|
408
921
|
display mode, and online state. Extra fields need explicit selection. Optional reads use a 500 ms
|
|
409
922
|
deadline by default. Unknown field names reject before platform reads. Collection never requests capture or permission and omits device IDs and labels.
|
|
410
923
|
|
|
411
|
-
External
|
|
412
|
-
|
|
413
|
-
|
|
924
|
+
External service integrations follow the [synchronous sink contract](#console-output-and-custom-sinks).
|
|
925
|
+
The application owns service mapping, network delivery, and SDK lifecycle.
|
|
926
|
+
|
|
927
|
+
## Privacy
|
|
928
|
+
|
|
929
|
+
Records contain time, run ID, sequence, area, scope, level, message, and selected context, data,
|
|
930
|
+
and Error fields after the configured redactor runs. Application policy determines which private
|
|
931
|
+
values remain. A run ID is a pseudonym, not an account permission or a guarantee of anonymity.
|
|
932
|
+
Do not use direct account identifiers in database names. The application supplies an opaque scope.
|
|
933
|
+
|
|
934
|
+
Default history stays in page memory until eviction, clear, or page exit. A memory outbox also loses
|
|
935
|
+
records on reload. IndexedDB history and outboxes are device storage and can survive restart.
|
|
936
|
+
History follows its budgets. Outbox records remain until acknowledgement, clear, or capacity loss;
|
|
937
|
+
outboxes have no age expiry. Stop preserves persistent records. Clear cannot erase exported files,
|
|
938
|
+
returned snapshots, records already sent, or copies held by application code.
|
|
939
|
+
|
|
940
|
+
Upload is disabled unless configured. The consumer selects the endpoint and every third party,
|
|
941
|
+
owns credentials, retry, backend retention, access, and deletion. The application must apply its
|
|
942
|
+
permission policy separately to local recording and background upload. Enabling one does not
|
|
943
|
+
grant permission for the other. Docket adds no consent UI or external service.
|
|
944
|
+
|
|
945
|
+
Delivery is at least once. The backend can receive duplicates and must deduplicate batch tokens
|
|
946
|
+
within the original account scope. Keep account databases and sender credentials separate after
|
|
947
|
+
forced sign-out, failed cleanup, and restart. An old account's records must never use a new
|
|
948
|
+
account's sender. Retry failed cleanup under the old account's known resource names.
|
|
414
949
|
|
|
415
950
|
## Errors
|
|
416
951
|
|
|
@@ -419,25 +954,32 @@ Logger writes never throw. Configuration errors and missing capabilities can thr
|
|
|
419
954
|
all sinks and reject with an aggregate error on failure. Hub-generated diagnostics use codes
|
|
420
955
|
`recursive-logging`, `root-logger-cache-limit`, and `stopped`. Callback failures keep their original errors.
|
|
421
956
|
|
|
422
|
-
|
|
423
|
-
and `rotation`.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
These failures do not
|
|
432
|
-
stop, history rejects cleanup and retains the lease. Pending source queries drain before retirement ends.
|
|
957
|
+
Standalone store errors have codes `open`, `write`, `read`, `quota`, `overflow`, `ownership`,
|
|
958
|
+
`clear`, `upgrade`, and `rotation`. Memory history rejects invalid query filters with `TypeError`.
|
|
959
|
+
Invalid memory-history options throw `TypeError`. The upload factory validates its options and
|
|
960
|
+
outbox before it schedules or sends work.
|
|
961
|
+
|
|
962
|
+
Upload flush rejects on the first storage, send, or acknowledgement failure.
|
|
963
|
+
A supplied `onError` receives each failed attempt once,
|
|
964
|
+
including an adapter `AbortError` while the sink is active. Sink stop instead rejects interrupted
|
|
965
|
+
flushes with `AbortError` without reporting an upload failure. Export and developer actions reject
|
|
966
|
+
to their caller. These failures do not remove retained local history.
|
|
433
967
|
|
|
434
968
|
## TypeScript
|
|
435
969
|
|
|
436
|
-
Declarations are provided for ESM and CommonJS. Core
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
970
|
+
Declarations are provided for ESM and CommonJS. Core, memory history, redaction, SIP, and devtools
|
|
971
|
+
need only ES2022 types. Upload and client need `AbortSignal` declarations from the target runtime.
|
|
972
|
+
Browser, persistence, and export entries use DOM types. No entry declaration imports a provider
|
|
973
|
+
package. The logger satisfies the existing workspace logger ports without an adapter.
|
|
974
|
+
Area inference rejects unknown area names.
|
|
975
|
+
Import `DocketOptions`, `Docket`, `HistoryClient`, `OutboxClient`, and `DocketUploadOptions` from
|
|
976
|
+
`@bazariodev/docket/client`. Hopper clients satisfy these local structural types.
|
|
977
|
+
Import `MemoryHistoryOptions` and `MemoryHistory` from `@bazariodev/docket/history`. Import
|
|
978
|
+
`UploadSinkOptions`, `UploadSinkStatus`, `UploadSink`, and `UploadOutbox` from
|
|
979
|
+
`@bazariodev/docket/upload`. Hopper's `Outbox<LogRecord>` satisfies the upload port.
|
|
980
|
+
Queue types are internal. Consumers do not supply or import them for memory history.
|
|
981
|
+
Import `LogRecord` from `@bazariodev/docket`. Use `createMemoryStore<LogRecord>` when selecting
|
|
982
|
+
Hopper's standalone store or rotation snapshots. The export source receives a query argument.
|
|
441
983
|
|
|
442
984
|
## License
|
|
443
985
|
|