@bazariodev/docket 0.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +742 -169
  3. package/dist/browser/index.cjs +2 -2
  4. package/dist/browser/index.js +2 -2
  5. package/dist/{chunk-LRCHwVuE.js → chunk---OIebMz.js} +31 -3
  6. package/dist/chunk---OIebMz.js.map +1 -0
  7. package/dist/{chunk-CEhpwtwJ.cjs → chunk-BMmKUicm.cjs} +9 -3
  8. package/dist/chunk-BMmKUicm.cjs.map +1 -0
  9. package/dist/{chunk-B-SP8Od_.js → chunk-CJmlBW-M.js} +69 -26
  10. package/dist/chunk-CJmlBW-M.js.map +1 -0
  11. package/dist/{chunk-DYcNnkOV.js → chunk-D1NggH6k.js} +2 -2
  12. package/dist/{chunk-DYcNnkOV.js.map → chunk-D1NggH6k.js.map} +1 -1
  13. package/dist/{chunk-Bvq4tF_F.cjs → chunk-DEyTe0f4.cjs} +68 -26
  14. package/dist/chunk-DEyTe0f4.cjs.map +1 -0
  15. package/dist/{chunk-BLf-eCDO.cjs → chunk-DLnBMRjo.cjs} +3 -1
  16. package/dist/chunk-DLnBMRjo.cjs.map +1 -0
  17. package/dist/{chunk-CSS3f4_M.js → chunk-Da877R_E.js} +4 -2
  18. package/dist/chunk-Da877R_E.js.map +1 -0
  19. package/dist/{chunk-7ld72rnl.js → chunk-De6BoS1P.js} +9 -3
  20. package/dist/chunk-De6BoS1P.js.map +1 -0
  21. package/dist/{chunk-CXZYNALm.cjs → chunk-Dl2KJDqt.cjs} +31 -2
  22. package/dist/chunk-Dl2KJDqt.cjs.map +1 -0
  23. package/dist/{chunk-DqK_3eFr.cjs → chunk-DlZYLPaU.cjs} +2 -2
  24. package/dist/{chunk-DqK_3eFr.cjs.map → chunk-DlZYLPaU.cjs.map} +1 -1
  25. package/dist/client/index.cjs +167 -0
  26. package/dist/client/index.cjs.map +1 -0
  27. package/dist/client/index.d.cts +40 -0
  28. package/dist/client/index.d.ts +40 -0
  29. package/dist/client/index.js +165 -0
  30. package/dist/client/index.js.map +1 -0
  31. package/dist/constants.d.cts +2 -4
  32. package/dist/constants.d.ts +2 -4
  33. package/dist/core/constants.d.cts +2 -0
  34. package/dist/core/constants.d.ts +2 -0
  35. package/dist/export/index.cjs +43 -15
  36. package/dist/export/index.cjs.map +1 -1
  37. package/dist/export/index.js +43 -15
  38. package/dist/export/index.js.map +1 -1
  39. package/dist/history/index.cjs +22 -1188
  40. package/dist/history/index.cjs.map +1 -1
  41. package/dist/history/index.d.cts +10 -112
  42. package/dist/history/index.d.ts +10 -112
  43. package/dist/history/index.js +22 -1188
  44. package/dist/history/index.js.map +1 -1
  45. package/dist/index.cjs +5 -5
  46. package/dist/index.d.cts +8 -4
  47. package/dist/index.d.ts +8 -4
  48. package/dist/index.js +5 -5
  49. package/dist/indexeddb/index.cjs +18 -7
  50. package/dist/indexeddb/index.cjs.map +1 -1
  51. package/dist/indexeddb/index.d.cts +19 -4
  52. package/dist/indexeddb/index.d.ts +19 -4
  53. package/dist/indexeddb/index.js +18 -8
  54. package/dist/indexeddb/index.js.map +1 -1
  55. package/dist/redaction/index.cjs +4 -2
  56. package/dist/redaction/index.cjs.map +1 -1
  57. package/dist/redaction/index.js +4 -2
  58. package/dist/redaction/index.js.map +1 -1
  59. package/dist/sip/index.cjs +1 -1
  60. package/dist/sip/index.js +1 -1
  61. package/dist/storage/contracts.d.cts +0 -4
  62. package/dist/storage/contracts.d.ts +0 -4
  63. package/dist/upload/constants.d.cts +17 -0
  64. package/dist/upload/constants.d.ts +17 -0
  65. package/dist/upload/index.cjs +246 -0
  66. package/dist/upload/index.cjs.map +1 -0
  67. package/dist/upload/index.d.cts +51 -0
  68. package/dist/upload/index.d.ts +51 -0
  69. package/dist/upload/index.js +244 -0
  70. package/dist/upload/index.js.map +1 -0
  71. package/package.json +25 -2
  72. package/dist/chunk-7ld72rnl.js.map +0 -1
  73. package/dist/chunk-B-SP8Od_.js.map +0 -1
  74. package/dist/chunk-BLf-eCDO.cjs.map +0 -1
  75. package/dist/chunk-Bvq4tF_F.cjs.map +0 -1
  76. package/dist/chunk-CEhpwtwJ.cjs.map +0 -1
  77. package/dist/chunk-CSS3f4_M.js.map +0 -1
  78. package/dist/chunk-CXZYNALm.cjs.map +0 -1
  79. package/dist/chunk-LRCHwVuE.js.map +0 -1
  80. package/dist/core/errors.d.cts +0 -8
  81. package/dist/core/errors.d.ts +0 -8
  82. package/dist/history/constants.d.cts +0 -35
  83. package/dist/history/constants.d.ts +0 -35
  84. package/dist/sip/constants.d.cts +0 -5
  85. 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 has no runtime dependencies, optional dependencies, or peer dependencies.
13
- Its runtime files and declarations do not import provider packages. The application selects
14
- providers and supplies their capabilities. History, conversion, persistence, SIP, and ZIP support
15
- are optional. Import only the entries that the application uses.
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 and [Hopper](https://www.npmjs.com/package/@bazariodev/hopper) for memory storage:
19
+ conversion:
19
20
 
20
21
  ```sh
21
- pnpm add @bazariodev/winnow @bazariodev/hopper
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 Baton for application ownership.
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
- Managed history uses Web Crypto and defaults to localStorage for its metadata.
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 { createLogging } from '@bazariodev/docket';
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 store = createMemoryStore();
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: [store],
271
+ sinks: [history],
49
272
  });
50
- const log = logging.getLogger('application', 'Connection');
51
- log.info('Connection ready', { state: 'online' });
52
273
  ```
53
274
 
54
- The later examples reuse `conversion`, `redactor`, and `store` from this example.
55
- Supply your application's area list and privacy policy. The default text rules are a starting
56
- point. They do not identify every private field in an application.
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,
320
+ });
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());
357
+ ```
358
+
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
- ## Export and application upload
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: store,
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 application owns upload. It supplies the endpoint, credentials, request format, response
178
- checks, cancellation, and timeout. A manual upload does not clear history. The package has no
179
- HTTP client, retry service, delivery acknowledgement, or durable upload queue.
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
- ```ts
182
- import type { LogRecord } from '@bazariodev/docket';
183
- import { createMemoryStore } from '@bazariodev/hopper';
184
- import * as archive from '@bazariodev/baler';
185
- import { createLogBundle } from '@bazariodev/docket/export';
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
- // The application supplies sendFile and reportError.
188
- const store = createMemoryStore<LogRecord>({
189
- overflowStrategy: 'rotate',
190
- async onRotate(source) {
191
- const file = await createLogBundle({
192
- source,
193
- redactor,
194
- files: [{ name: 'application.log', areas: ['application'] }],
195
- }, archive);
196
- await sendFile(file);
197
- },
198
- onError: reportError,
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
- Here, `redactor` is the same policy used for recording. Rotation removes a retained batch before
203
- it calls the application. Recording continues while that callback is pending. Further capacity
204
- pressure uses oldest-record eviction until the callback settles. A failed upload reports
205
- `rotation`; it does not restore records or retry. Clear and stop invalidate the snapshot without
206
- waiting for networking. An upload that already started belongs to the application.
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 store = createIndexedDbLogStore(
215
- { databaseName: 'example-diagnostics' },
216
- createIndexedDbStore,
217
- );
218
- // Attach the store to a logging hub before recording.
219
- await store.flush();
220
- const status = store.getStatus();
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,48 @@ 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
- ## Account history
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.
237
825
 
238
- Use `/history` when the package must own account cleanup, keys, and reload state. Acquire an
239
- application lease first. Use a stable key for each application, environment, and tenant.
826
+ ## Record metadata
240
827
 
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';
828
+ Pass flags in the third argument. The application can use them to select records in a sink.
248
829
 
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');
830
+ ```js
831
+ logger.warn('request.failed', { error }, { metadata: { report: false } });
832
+ // A sink can skip a record when record.metadata?.report === false.
271
833
  ```
272
834
 
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.
835
+ Use `undefined` for data when the call has no diagnostic data. Options must be a non-null
836
+ object and cannot be an array. Docket reads only the own `metadata` data property. It ignores
837
+ other and inherited properties. An accessor is invalid. Omitted or undefined options and empty
838
+ or undefined metadata add no property to the record.
839
+
840
+ Metadata must be a plain object or an object with a null prototype. It can have at most 16 own,
841
+ enumerable string data properties. Each name must contain 1 to 64 JavaScript string units.
842
+ Each value must be a boolean. Symbols and accessors are invalid. Use static, non-secret names.
843
+ Do not put diagnostic values or user identifiers in flags. Invalid input drops the record and
844
+ reports source `data`. Docket copies and freezes the flags before it calls lazy data.
845
+ Inspection runs under the reentry guard, and admission is checked again before lazy data.
846
+
847
+ Flags stay unchanged when diagnostic data is truncated. They count toward the total record size.
848
+ If the headers, flags, and truncation marker cannot fit, Docket drops the record and reports
849
+ source `redactor`. Custom redactors must support metadata before flagged calls are used. They
850
+ must return the same flags and include them in the record-size calculation. Docket checks the
851
+ returned flags against its original frozen copy. Changed, added, or missing flags drop the record
852
+ and report source `redactor`. Key order and object identity can differ. Text formatting does not
853
+ show flags. Structured records retain them.
324
854
 
325
855
  ## Behavior
326
856
 
327
857
  `isActive` is an optional admission guard. Returning `false` stops a record before conversion or
328
- delivery. The hub checks it again after application callbacks. Managed history supplies this guard.
858
+ delivery. The hub checks it again after application callbacks. `createDocket` captures this
859
+ function from the original options, including prototype methods, and calls it with that options object as
860
+ its receiver. State changes on the original object take effect on later checks. A replacement
861
+ method does not replace the captured gate. If the gate stops the client, the current record is
862
+ blocked before delivery. The application supplies this guard when it needs one.
329
863
 
330
864
  - Levels are `debug`, `info`, `warn`, and `error`. Default area level is `info`.
331
865
  - Data functions run only after admission. No sinks, stop, levels, and storm limits can prevent admission.
@@ -342,7 +876,8 @@ delivery. The hub checks it again after application callbacks. Managed history s
342
876
  - Default storm allowance is 100 records per area/scope each second. One timer reports suppression.
343
877
  - Sink failures do not stop other sinks. Recursive logging from sinks and reporters is rejected.
344
878
 
345
- The store defaults and recovery rules below describe the Hopper composition shown above.
879
+ The standalone store defaults and recovery rules below describe the Hopper provider.
880
+ They do not apply to the FIFO history helper or outbox adapters.
346
881
  Other providers must state their supported behavior.
347
882
 
348
883
  - Queries use inclusive time bounds and order by time, run ID, then sequence. Empty areas select nothing.
@@ -361,9 +896,11 @@ Other providers must state their supported behavior.
361
896
  | Entry | Exports |
362
897
  | --- | --- |
363
898
  | Root | `createLogging`, `createConsoleSink`, `formatRecord` |
364
- | `/redaction` | `createRedactor(policy, winnow)` |
365
- | `/indexeddb` | `createIndexedDbLogStore(options, createStore)` |
366
- | `/history` | `createLogHistory` |
899
+ | `/client` | `createDocket(options)` |
900
+ | `/redaction` | `createRedactor(policy, providers)` |
901
+ | `/indexeddb` | `createIndexedDbLogStore(options, createStore)`, `clearIndexedDbLogStore(options, clearStore)` |
902
+ | `/history` | `createMemoryHistory(options)` |
903
+ | `/upload` | `createUploadSink(options, outbox)` |
367
904
  | `/export` | `createLogBundle(options, archive)` |
368
905
  | `/browser` | `captureGlobalErrors`, `flushOnPageHide`, `collectBrowserEnvironment`, `downloadBlob` |
369
906
  | `/sip` | `createSipTools(textRules)`, `describeSipMessage`. The tools expose `redactSipMessage`, `describeSipMessage`, and `SDP_TEXT_RULES`. |
@@ -371,12 +908,14 @@ Other providers must state their supported behavior.
371
908
 
372
909
  | Component | Options and methods |
373
910
  | --- | --- |
911
+ | 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
912
  | 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
- | Logger | `debug`, `info`, `warn`, `error` accept `(message, data?)`. `child({ scope?, context? })` adds bindings. |
913
+ | Logger | `debug`, `info`, `warn`, `error` accept `(message, data?, options?)`. `child({ scope?, context? })` adds bindings. |
376
914
  | Redactor | Policy: `version`, `hash`, `fields`, `unknownFields`, `text`, `limits`. Methods: `redact`, `redactValue`, `redactText`, `mask`, optional `hash`. Resolved `limits` and `version` are public. |
377
915
  | 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
- | History | Required `lease`, `logging`, `createRedactor`, and `stores`. Optional `createSinks`, `initialMode`, database/storage ports, `memory`, `startup`, `persistent`, rotation callbacks, `onChange`, `onError`. Methods: `getLogger`, `resume`, `setOwner`, `logout`, `setMode`, `clear`, `retryCleanup`, `getSession`, `getStatus`, `stop`. |
916
+ | IndexedDB store | Memory options plus required `databaseName` and `lockName`, optional `indexedDB`, `locks`, `allowUnprotected`, batch/queue/interval/count limits, and `fallback`. |
917
+ | Memory history | Required `maxRecords`. Queue creation is internal. Methods: `write`, `query`, `clear`. |
918
+ | Upload sink | Required `maxBatchRecords`, `intervalMs`, `send`, and an outbox. Optional `onError` and `scheduler`. Methods: `write`, `flush`, `stop`, `getStatus`. |
380
919
  | Bundle | Required `source`, `redactor`, `files`, and the separate archive capability. Optional time bounds, `manifest`, `fileName`, `maxInputSize`, clock, scheduler. Returns `blob`, `fileName`, file counts. |
381
920
  | Supplied Hopper copy helper | `copyRecords(from, to, { maxChunkRecords?, maxChunkSize? })`; defaults are 100 records and 1,000,000 JSON characters. It flushes between chunks. |
382
921
  | Console sink | Optional minimum `level` and injected `console`. |
@@ -397,20 +936,45 @@ Bundle filenames cannot contain paths, reserved device names, or manifest varian
397
936
  uses NFC and ignores case. Empty files are omitted; the manifest retains their zero counts.
398
937
  File names, each file's area list, and the download name are checked before the source query. Its default timestamp is
399
938
  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. Input is limited
401
- to 32,000,000 UTF-8 bytes by default, including the manifest. Baler checks this limit after
402
- rendering and before compression. It does not bound renderer allocations or peak heap use.
939
+ Custom renderers are synchronous. Built-in formatting yields during large files. The
940
+ [export byte limit](#export-and-download) applies during formatting and before compression.
403
941
  The supplied Baler implementation uses raw deflate when available and stored entries otherwise.
404
942
  The manifest contains ranges, counts, truncation, warnings, policy versions, store status, and
405
943
  redacted application metadata. It contains no history key.
406
944
 
945
+ `formatRecord` produces human-readable text with flat labels. Context and field keys can repeat
946
+ header names such as `runId` or `message`. Choose distinct data names and use structured records
947
+ for machine parsing; the text format does not guarantee unique keys across those sections.
948
+
407
949
  Browser listeners are explicit and return cleanup. Environment defaults are language, UTC offset,
408
950
  display mode, and online state. Extra fields need explicit selection. Optional reads use a 500 ms
409
951
  deadline by default. Unknown field names reject before platform reads. Collection never requests capture or permission and omits device IDs and labels.
410
952
 
411
- External services use an application-owned `LogSink.write(record)` implementation. Pass it to
412
- `sinks` or `addSink`. It receives redacted, frozen records. The application owns service mapping,
413
- network delivery, and SDK lifecycle. No additional event interface is required.
953
+ External service integrations follow the [synchronous sink contract](#console-output-and-custom-sinks).
954
+ The application owns service mapping, network delivery, and SDK lifecycle.
955
+
956
+ ## Privacy
957
+
958
+ Records contain time, run ID, sequence, area, scope, level, message, and selected context, data,
959
+ and Error fields after the configured redactor runs. Application policy determines which private
960
+ values remain. A run ID is a pseudonym, not an account permission or a guarantee of anonymity.
961
+ Do not use direct account identifiers in database names. The application supplies an opaque scope.
962
+
963
+ Default history stays in page memory until eviction, clear, or page exit. A memory outbox also loses
964
+ records on reload. IndexedDB history and outboxes are device storage and can survive restart.
965
+ History follows its budgets. Outbox records remain until acknowledgement, clear, or capacity loss;
966
+ outboxes have no age expiry. Stop preserves persistent records. Clear cannot erase exported files,
967
+ returned snapshots, records already sent, or copies held by application code.
968
+
969
+ Upload is disabled unless configured. The consumer selects the endpoint and every third party,
970
+ owns credentials, retry, backend retention, access, and deletion. The application must apply its
971
+ permission policy separately to local recording and background upload. Enabling one does not
972
+ grant permission for the other. Docket adds no consent UI or external service.
973
+
974
+ Delivery is at least once. The backend can receive duplicates and must deduplicate batch tokens
975
+ within the original account scope. Keep account databases and sender credentials separate after
976
+ forced sign-out, failed cleanup, and restart. An old account's records must never use a new
977
+ account's sender. Retry failed cleanup under the old account's known resource names.
414
978
 
415
979
  ## Errors
416
980
 
@@ -419,25 +983,34 @@ Logger writes never throw. Configuration errors and missing capabilities can thr
419
983
  all sinks and reject with an aggregate error on failure. Hub-generated diagnostics use codes
420
984
  `recursive-logging`, `root-logger-cache-limit`, and `stopped`. Callback failures keep their original errors.
421
985
 
422
- Store errors have codes `open`, `write`, `read`, `quota`, `overflow`, `ownership`, `clear`, `upgrade`,
423
- and `rotation`. History errors use `metadata`, `cleanup`, `stale`, `setup`, `migration`, `stopped`,
424
- and `rotation`. Degraded history cleanup resolves with `cleanupPending: true` and reports the error.
425
- Failed migration rejects and keeps the previous session. Export and developer actions reject to the caller.
426
- All acquired stores stay under history cleanup, including setup and temporary cleanup stores.
427
- A failed store stop remains available for retry and prevents application lease release.
428
- Managed history reports application sink-stop failures as `setup` errors after its store stops.
429
- The error cause retains the hub's AggregateError and its original sink errors. Store flush and
430
- stop reports also retain their original failures as the cause.
431
- These failures do not prevent account erasure or application lease release. If the store cannot
432
- stop, history rejects cleanup and retains the lease. Pending source queries drain before retirement ends.
986
+ Standalone store errors have codes `open`, `write`, `read`, `quota`, `overflow`, `ownership`,
987
+ `clear`, `upgrade`, and `rotation`. Memory history rejects invalid query filters with `TypeError`.
988
+ Invalid memory-history options throw `TypeError`. The upload factory validates its options and
989
+ outbox before it schedules or sends work.
990
+
991
+ Upload flush rejects on the first storage, send, or acknowledgement failure.
992
+ A supplied `onError` receives each failed attempt once,
993
+ including an adapter `AbortError` while the sink is active. Sink stop instead rejects interrupted
994
+ flushes with `AbortError` without reporting an upload failure. Export and developer actions reject
995
+ to their caller. These failures do not remove retained local history.
433
996
 
434
997
  ## TypeScript
435
998
 
436
- Declarations are provided for ESM and CommonJS. Core declarations need only ES2022 types.
437
- No entry declaration imports a provider package. Browser, persistence, history, and export entries need DOM types. The logger satisfies the existing
438
- workspace logger ports without an adapter. Area inference rejects unknown area names.
439
- Use `createMemoryStore<LogRecord>` for a store that supplies rotation snapshots to Docket export.
440
- Import `LogRecord` from `@bazariodev/docket`. The export source receives a query argument.
999
+ Declarations are provided for ESM and CommonJS. Core, memory history, redaction, SIP, and devtools
1000
+ need only ES2022 types. Upload and client need `AbortSignal` declarations from the target runtime.
1001
+ Browser, persistence, and export entries use DOM types. No entry declaration imports a provider
1002
+ package. The logger satisfies the existing workspace logger ports without an adapter.
1003
+ Area inference rejects unknown area names.
1004
+ Import `DocketOptions`, `Docket`, `HistoryClient`, `OutboxClient`, and `DocketUploadOptions` from
1005
+ `@bazariodev/docket/client`. Hopper clients satisfy these local structural types.
1006
+ Import `MemoryHistoryOptions` and `MemoryHistory` from `@bazariodev/docket/history`. Import
1007
+ `UploadSinkOptions`, `UploadSinkStatus`, `UploadSink`, and `UploadOutbox` from
1008
+ `@bazariodev/docket/upload`. Hopper's `Outbox<LogRecord>` satisfies the upload port.
1009
+ Queue types are internal. Consumers do not supply or import them for memory history.
1010
+ Import `LogRecord`, `LogMetadata`, and `LogOptions` from `@bazariodev/docket`.
1011
+ `LogOptions.metadata` accepts explicit or conditional `undefined` with `exactOptionalPropertyTypes`.
1012
+ Use `createMemoryStore<LogRecord>` when selecting
1013
+ Hopper's standalone store or rotation snapshots. The export source receives a query argument.
441
1014
 
442
1015
  ## License
443
1016