@oxidezap/baileyrs 0.2.11 → 0.2.13

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.
@@ -31,6 +31,7 @@ import { makeBridgeClientOwner } from './bridge-client-owner.js';
31
31
  import { warnUnsupportedConfig } from './unsupported-config.js';
32
32
  import { wrapBridgeClient } from './bridge-error-boundary.js';
33
33
  import { makeTerminalCloseReporter } from './terminal-close-reporter.js';
34
+ import { mapConnectFailureToDisconnect } from './terminal-close.js';
34
35
  import { makeEventHandlers } from './events.js';
35
36
  import { makeGroupMethods } from './groups.js';
36
37
  import { makeInternalMethods, makeUnexpectedErrorReporter } from './internals.js';
@@ -71,6 +72,29 @@ const browserToPlatformType = (browser) => {
71
72
  return 'CHROME';
72
73
  }
73
74
  };
75
+ const COMPLETION_FAILURE_CODES = new Map([
76
+ ['Generic', 400],
77
+ ['LoggedOut', 401],
78
+ ['TempBanned', 402],
79
+ ['AccountLocked', 403],
80
+ ['UnknownLogout', 406],
81
+ ['ClientOutdated', 405],
82
+ ['BadUserAgent', 409],
83
+ ['CatExpired', 413],
84
+ ['CatInvalid', 414],
85
+ ['NotFound', 415],
86
+ ['ClientUnknown', 418],
87
+ ['InternalServerError', 500],
88
+ ['Experimental', 501],
89
+ ['ServiceUnavailable', 503]
90
+ ]);
91
+ const completionFailureCode = (reason) => {
92
+ const named = COMPLETION_FAILURE_CODES.get(reason);
93
+ if (named !== undefined)
94
+ return named;
95
+ const unknown = /^Unknown\((-?\d+)\)$/.exec(reason)?.[1];
96
+ return unknown === undefined ? undefined : Number(unknown);
97
+ };
74
98
  /** Build the ws EventEmitter with auto-enable raw node forwarding */
75
99
  const makeWASocket = (config) => {
76
100
  const fullConfig = { ...DEFAULT_CONNECTION_CONFIG, ...config };
@@ -261,6 +285,37 @@ const makeWASocket = (config) => {
261
285
  let autoReconnectEnabled = true;
262
286
  /** Owns reporting the terminal close: once, after teardown, never not at all. */
263
287
  const terminalClose = makeTerminalCloseReporter({ logger });
288
+ const runCompletionError = (completion) => {
289
+ let statusCode = DisconnectReason.connectionClosed;
290
+ let message = 'Connection closed';
291
+ if (completion.reason === 'unknown') {
292
+ message = `Connection run ended: ${completion.detail}`;
293
+ }
294
+ else if (completion.reason === 'stopped') {
295
+ message = 'Connection run stopped';
296
+ }
297
+ else if (completion.reason === 'already-running') {
298
+ message = 'Connection run was already running';
299
+ }
300
+ else if (completion.reason === 'auto-reconnect-disabled') {
301
+ const protocol = completion.protocolError;
302
+ if (protocol?.kind === 'conflict') {
303
+ statusCode = DisconnectReason.connectionReplaced;
304
+ message = 'Connection replaced';
305
+ }
306
+ else if (protocol?.kind === 'stream-error') {
307
+ statusCode = mapConnectFailureToDisconnect(protocol.code);
308
+ }
309
+ else if (protocol?.kind === 'connect-failure') {
310
+ statusCode = mapConnectFailureToDisconnect(completionFailureCode(protocol.reason));
311
+ }
312
+ else if (completion.connection?.kind === 'server-close') {
313
+ message = completion.connection.reason;
314
+ }
315
+ }
316
+ return new Boom(message, { statusCode, data: { runCompletion: completion } });
317
+ };
318
+ const reportTerminalClose = (error, publish) => terminalClose.reportAfter(() => owner.close(error).finally(() => initPromise), publish);
264
319
  /**
265
320
  * Held in a reporter rather than captured, because `sock.onUnexpectedError`
266
321
  * is an assignable property: a consumer that replaces it has to be the one
@@ -401,7 +456,7 @@ const makeWASocket = (config) => {
401
456
  // is still owned. This waits for the real teardown, and cannot
402
457
  // deadlock because `reportAfter` runs it detached; nothing in the
403
458
  // teardown is waiting on this.
404
- terminalClose.reportAfter(() => owner.close(error).finally(() => initPromise), publish);
459
+ reportTerminalClose(error, publish);
405
460
  },
406
461
  isAutoReconnectEnabled: () => autoReconnectEnabled,
407
462
  // Timers the dispatcher armed outlive the events that armed them, and
@@ -454,7 +509,13 @@ const makeWASocket = (config) => {
454
509
  }
455
510
  if (useNativeMemory)
456
511
  logger.debug('auth: using socket-local native memory backend');
457
- const created = await createWhatsAppClient(makeTransport(fullConfig), makeHttpClient(fullConfig), eventHandlers, bridgeStore, fullConfig.cache ?? null, fullConfig.version, fullConfig.wantedPreKeyCount ?? null);
512
+ const created = await createWhatsAppClient(makeTransport(fullConfig), makeHttpClient(fullConfig), eventHandlers, bridgeStore, fullConfig.cache ?? null, fullConfig.version, fullConfig.wantedPreKeyCount ?? null,
513
+ // Passed through exactly as configured, never normalized by truthiness:
514
+ // the bridge only honours a literal `true` here and rejects any
515
+ // other truthy value at construction, so a `!!`/ternary-style
516
+ // coercion could promote a malformed opt-out into an opt-in.
517
+ // Absent stays strict.
518
+ fullConfig.dangerSkipCertChainVerify);
458
519
  // `end()` can land while the client is still being built — a `sock.end()`
459
520
  // or `await using` right after `makeWASocket()` does exactly that. When
460
521
  // it has, `adopt` frees this client and tells us to stop: nothing else
@@ -521,23 +582,46 @@ const makeWASocket = (config) => {
521
582
  // starting the read loop now would run against a handle about to go.
522
583
  if (owner.isClosing())
523
584
  return;
524
- // `run()` spawns the connect/handshake/read/reconnect loop as a
525
- // background task and returns `void` it deliberately is not `async`,
526
- // so that it does not hold a wasm-bindgen borrow on `self` that would
527
- // block `disconnect()`.
528
- //
529
- // Consequence: the loop's exit is not observable from here. The engine
530
- // clears `enable_auto_reconnect` and breaks out on every terminal
531
- // disconnect (conflict/401/409/516, and any `<failure>` whose reason is
532
- // not 500/503), and when it does, the `WasmWhatsAppClient` is dead
533
- // weight that only `sock.end()` can free — nothing else can, because
534
- // the bridge holds the JS event callbacks as wasm-bindgen externrefs,
535
- // those close over `ctx`, and `ctx` closes over `client`, so the cycle
536
- // crosses the JS/wasm boundary and no `FinalizationRegistry` fires.
537
- // Freeing that automatically needs the bridge to expose loop completion
538
- // (a terminal callback or an awaitable handle); until it does, the
539
- // consumer has to call `sock.end()` on a terminal close.
585
+ // `run()` deliberately returns immediately so callers can use the
586
+ // client while supervision owns its background task. Registering the
587
+ // completion observer after it is started is safe: bridge 0.21.0 admits
588
+ // late observers against the stored result for this run generation.
540
589
  created.run();
590
+ const observedClient = created;
591
+ void created
592
+ .waitForRunCompletion()
593
+ .then(completion => {
594
+ // The owner identity is the socket generation fence. A completion
595
+ // from a client that teardown already released must never close a
596
+ // later socket using the same auth state.
597
+ if (owner.isClosing() || owner.peek() !== observedClient)
598
+ return;
599
+ const error = runCompletionError(completion);
600
+ reportTerminalClose(error, () => ev.emit('connection.update', {
601
+ connection: 'close',
602
+ lastDisconnect: { error, date: new Date() }
603
+ }));
604
+ }, error => {
605
+ if (owner.isClosing() || owner.peek() !== observedClient)
606
+ return;
607
+ const closeError = new Boom('Connection run ended without a completion result', {
608
+ statusCode: DisconnectReason.connectionClosed
609
+ });
610
+ reportTerminalClose(closeError, () => ev.emit('connection.update', {
611
+ connection: 'close',
612
+ lastDisconnect: { error: closeError, date: new Date() }
613
+ }));
614
+ try {
615
+ logger.error({ err: error }, 'bridge run completion observation failed');
616
+ }
617
+ catch {
618
+ // A consumer logger cannot prevent the terminal cleanup above.
619
+ }
620
+ })
621
+ .catch(() => {
622
+ // The reporter contains teardown and publish failures; this final guard
623
+ // also contains a consumer logger that throws from an observation path.
624
+ });
541
625
  initialized = true;
542
626
  };
543
627
  /**
@@ -1,6 +1,13 @@
1
1
  /**
2
- * Reports the terminal `connection.update { close }` — exactly once, after
3
- * teardown, and never silently not at all.
2
+ * Reports the terminal `connection.update { close }` — exactly once, and
3
+ * never silently not at all.
4
+ *
5
+ * The accepted claim publishes after its teardown settles, so a consumer that
6
+ * answers the close with a replacement socket does not overlap the old one's
7
+ * release. The watchdog is the deliberate exception: past the timeout the
8
+ * close goes out with teardown still running (and logged), because losing the
9
+ * event entirely is the worse failure. Nothing here promises that every close
10
+ * lands after every resource is released — only that at most one close lands.
4
11
  *
5
12
  * That sentence is the whole contract this branch sells, and getting it wrong
6
13
  * has two opposite failure modes, both bad:
@@ -8,9 +15,8 @@
8
15
  * - **Not reported.** The consumer's handler never runs, so it never builds a
9
16
  * replacement socket. A bot offline with nothing in its logs — the original
10
17
  * bug this branch exists to fix.
11
- * - **Reported early, or twice.** The replacement socket overlaps the old
12
- * one's auth-store flush and `free()`, or every listener sees two terminal
13
- * notifications for one socket and a handler that cleans up on close loops.
18
+ * - **Reported twice.** Every listener sees two terminal notifications for
19
+ * one socket and a handler that cleans up on close loops.
14
20
  *
15
21
  * Keeping both away used to be inline logic split across the dispatcher hook
16
22
  * and `logout()`, sharing a counter and a promise. Nine separate bugs came out
@@ -41,13 +47,26 @@ export interface TerminalCloseReporter {
41
47
  *
42
48
  * Never rejects and never leaves `publish` uncalled: teardown failures are
43
49
  * logged, a throwing listener is contained, and a teardown that hangs is
44
- * cut short by the watchdog.
50
+ * reported past by the watchdog.
51
+ *
52
+ * Idempotent per socket: the first claim wins and every later call is
53
+ * ignored entirely — neither its teardown nor its publish is invoked. A
54
+ * socket never gets a second generation — a terminal close means "build a
55
+ * new socket" — so a second terminal event (a logout racing a dispatcher
56
+ * close, a late duplicate dispatch) must not publish again.
57
+ *
58
+ * The watchdog bounds how long `published()` waits, not the teardown
59
+ * itself: it neither cancels nor settles the underlying teardown, and a
60
+ * teardown that finishes late publishes nothing further.
45
61
  */
46
62
  reportAfter: (teardown: () => Promise<void>, publish: () => void) => void;
47
63
  /**
48
64
  * Publish immediately, for a close nothing else will announce — a `logout()`
49
65
  * with no live client, or one whose `logout()` threw before the bridge
50
66
  * dispatched anything.
67
+ *
68
+ * Part of the same single claim as `reportAfter`: a no-op when a close
69
+ * was already claimed.
51
70
  */
52
71
  reportNow: (publish: () => void) => void;
53
72
  /**
@@ -63,11 +82,13 @@ export interface TerminalCloseReporter {
63
82
  */
64
83
  hasReported: () => boolean;
65
84
  /**
66
- * Settles once the most recent report has been published — the moment the
85
+ * Settles once the single report has been published — the moment the
67
86
  * consumer sees it, not the moment teardown finishes, so a waiter is not
68
87
  * left hanging when the watchdog is what released the event.
69
88
  *
70
- * Resolves immediately when nothing has been reported.
89
+ * Resolves immediately when nothing has been reported. Every waiter
90
+ * observes the same promise, so concurrent `logout()` and dispatcher
91
+ * paths cannot split across generations.
71
92
  */
72
93
  published: () => Promise<void>;
73
94
  }
@@ -1,6 +1,13 @@
1
1
  /**
2
- * Reports the terminal `connection.update { close }` — exactly once, after
3
- * teardown, and never silently not at all.
2
+ * Reports the terminal `connection.update { close }` — exactly once, and
3
+ * never silently not at all.
4
+ *
5
+ * The accepted claim publishes after its teardown settles, so a consumer that
6
+ * answers the close with a replacement socket does not overlap the old one's
7
+ * release. The watchdog is the deliberate exception: past the timeout the
8
+ * close goes out with teardown still running (and logged), because losing the
9
+ * event entirely is the worse failure. Nothing here promises that every close
10
+ * lands after every resource is released — only that at most one close lands.
4
11
  *
5
12
  * That sentence is the whole contract this branch sells, and getting it wrong
6
13
  * has two opposite failure modes, both bad:
@@ -8,9 +15,8 @@
8
15
  * - **Not reported.** The consumer's handler never runs, so it never builds a
9
16
  * replacement socket. A bot offline with nothing in its logs — the original
10
17
  * bug this branch exists to fix.
11
- * - **Reported early, or twice.** The replacement socket overlaps the old
12
- * one's auth-store flush and `free()`, or every listener sees two terminal
13
- * notifications for one socket and a handler that cleans up on close loops.
18
+ * - **Reported twice.** Every listener sees two terminal notifications for
19
+ * one socket and a handler that cleans up on close loops.
14
20
  *
15
21
  * Keeping both away used to be inline logic split across the dispatcher hook
16
22
  * and `logout()`, sharing a counter and a promise. Nine separate bugs came out
@@ -35,9 +41,24 @@ export const TERMINAL_CLOSE_PUBLISH_TIMEOUT_MS = 60000;
35
41
  export const makeTerminalCloseReporter = (opts) => {
36
42
  const { logger } = opts;
37
43
  const publishTimeoutMs = opts.publishTimeoutMs ?? TERMINAL_CLOSE_PUBLISH_TIMEOUT_MS;
38
- /** Claims, not deliveries — see `hasReported`. */
44
+ /**
45
+ * Claims, not deliveries — see `hasReported`. At most one: a socket has a
46
+ * single terminal generation, so the first claim wins and later ones are
47
+ * ignored rather than published.
48
+ */
39
49
  let claimed = 0;
40
50
  let publishedPromise;
51
+ /**
52
+ * True once the single terminal generation has been claimed. Deliberately
53
+ * log-free: a duplicate arrives on a path whose logger is
54
+ * consumer-replaceable, and an ignored signal must never throw.
55
+ */
56
+ const claim = () => {
57
+ if (claimed > 0)
58
+ return false;
59
+ claimed++;
60
+ return true;
61
+ };
41
62
  /** One publish per claim, whatever gets there first, and never throwing. */
42
63
  const makeOnce = (publish, settle) => {
43
64
  let done = false;
@@ -59,7 +80,8 @@ export const makeTerminalCloseReporter = (opts) => {
59
80
  };
60
81
  return {
61
82
  reportAfter: (teardown, publish) => {
62
- claimed++;
83
+ if (!claim())
84
+ return;
63
85
  let settle;
64
86
  publishedPromise = new Promise(resolve => {
65
87
  settle = resolve;
@@ -94,7 +116,8 @@ export const makeTerminalCloseReporter = (opts) => {
94
116
  }
95
117
  },
96
118
  reportNow: publish => {
97
- claimed++;
119
+ if (!claim())
120
+ return;
98
121
  let settle;
99
122
  publishedPromise = new Promise(resolve => {
100
123
  settle = resolve;
@@ -1,39 +1,5 @@
1
- /**
2
- * Which bridge disconnects end the socket for good.
3
- *
4
- * This mirrors one decision that lives in the Rust engine, so keep it honest:
5
- * `whatsapp-rust` clears `enable_auto_reconnect` and breaks out of its run loop
6
- * on exactly these, and retries everything else on the WA Web Fibonacci backoff
7
- * (`client/lifecycle.rs`).
8
- *
9
- * The socket layer needs the answer because `WasmWhatsAppClient.run()` returns
10
- * `void` — it spawns the loop as a background task, so the loop's exit is not
11
- * observable from JS (`whatsapp_rust_bridge.d.ts`, `run(): void`). Until the
12
- * bridge exposes loop completion, this table is how the socket knows a client
13
- * has become dead weight that only `free()` can reclaim.
14
- *
15
- * The upstream Baileys contract this buys us: `connection.update { close }`
16
- * means "this socket is finished, build a new one" — which is what every
17
- * consumer written against upstream already assumes, because upstream has no
18
- * auto-reconnect at all. Transient drops therefore never surface as `close`;
19
- * they surface as `connecting`.
20
- *
21
- * Events deliberately absent from the terminal set, and why:
22
- * - `disconnected` — the engine only dispatches `Event::Disconnected` for an
23
- * *unexpected* loop exit, and every terminal path sets `expected_disconnect`
24
- * first, which suppresses it (`client/lifecycle.rs`). So this one is the
25
- * "engine is retrying" signal — with one exception the dispatcher handles:
26
- * under `setAutoReconnect(false)` the run loop dispatches `Disconnected`
27
- * and only *then* tests the flag and breaks out, which makes the very same
28
- * event terminal. Absence from this list means "not terminal on its own",
29
- * not "never terminal".
30
- * - `streamError` — reaches JS only from the engine's catch-all `<stream:error>`
31
- * branch (unknown code, `<ack/>`, `<xml-not-well-formed>`); the coded ones
32
- * (401/409/515/516/429/503) dispatch their own events instead. Every case
33
- * that gets here keeps the connection or recycles it deliberately.
34
- * - `pairError` — pairing failed, but the engine keeps its loop and re-emits a
35
- * QR; nothing about the client is dead.
36
- */
37
1
  /** True when the engine will keep retrying after this `<failure>`. */
38
2
  export declare const isReconnectableConnectFailure: (reason: number | undefined) => boolean;
3
+ /** Map a typed bridge connect-failure code to Baileys' close status. */
4
+ export declare const mapConnectFailureToDisconnect: (reason: number | undefined) => number;
39
5
  //# sourceMappingURL=terminal-close.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { DisconnectReason } from '../Types/index.js';
1
2
  /**
2
3
  * Which bridge disconnects end the socket for good.
3
4
  *
@@ -6,11 +7,10 @@
6
7
  * on exactly these, and retries everything else on the WA Web Fibonacci backoff
7
8
  * (`client/lifecycle.rs`).
8
9
  *
9
- * The socket layer needs the answer because `WasmWhatsAppClient.run()` returns
10
- * `void` it spawns the loop as a background task, so the loop's exit is not
11
- * observable from JS (`whatsapp_rust_bridge.d.ts`, `run(): void`). Until the
12
- * bridge exposes loop completion, this table is how the socket knows a client
13
- * has become dead weight that only `free()` can reclaim.
10
+ * The socket layer needs the answer for bridge events that classify a terminal
11
+ * close before the supervised run completion is observed. Bridge 0.21.0 also
12
+ * exposes `waitForRunCompletion()` for exits with no terminal event; that
13
+ * observer uses the same terminal-close reporter and owner as this table.
14
14
  *
15
15
  * The upstream Baileys contract this buys us: `connection.update { close }`
16
16
  * means "this socket is finished, build a new one" — which is what every
@@ -48,4 +48,28 @@ const RECONNECTABLE_CONNECT_FAILURE_REASONS = new Set([
48
48
  ]);
49
49
  /** True when the engine will keep retrying after this `<failure>`. */
50
50
  export const isReconnectableConnectFailure = (reason) => reason !== undefined && RECONNECTABLE_CONNECT_FAILURE_REASONS.has(reason);
51
+ /** Map a typed bridge connect-failure code to Baileys' close status. */
52
+ export const mapConnectFailureToDisconnect = (reason) => {
53
+ switch (reason) {
54
+ case 401:
55
+ case 403:
56
+ case 406:
57
+ return DisconnectReason.loggedOut;
58
+ case 402:
59
+ return DisconnectReason.forbidden;
60
+ case 405:
61
+ return 405;
62
+ case 411:
63
+ return DisconnectReason.multideviceMismatch;
64
+ case 503:
65
+ case 501:
66
+ return DisconnectReason.unavailableService;
67
+ case 408:
68
+ return DisconnectReason.timedOut;
69
+ case 515:
70
+ return DisconnectReason.restartRequired;
71
+ default:
72
+ return DisconnectReason.connectionClosed;
73
+ }
74
+ };
51
75
  //# sourceMappingURL=terminal-close.js.map
@@ -33,7 +33,7 @@ export declare const UNSUPPORTED_CONFIG_KEYS: readonly ["keepAliveIntervalMs", "
33
33
  * member of `SocketConfig` belongs to neither list — which is exactly how the
34
34
  * first version of the catalog shipped twelve keys short.
35
35
  */
36
- export declare const READ_CONFIG_KEYS: readonly ["waWebSocketUrl", "options", "logger", "version", "browser", "pushName", "auth", "cache", "deviceProps", "wantedPreKeyCount", "emitOwnEvents", "shouldIgnoreJid", "defaultQueryTimeoutMs", "transactionOpts", "makeSignalRepository"];
36
+ export declare const READ_CONFIG_KEYS: readonly ["waWebSocketUrl", "options", "logger", "version", "browser", "pushName", "auth", "cache", "deviceProps", "wantedPreKeyCount", "dangerSkipCertChainVerify", "emitOwnEvents", "shouldIgnoreJid", "defaultQueryTimeoutMs", "transactionOpts", "makeSignalRepository"];
37
37
  /**
38
38
  * Which unsupported options this caller actually passed.
39
39
  *
@@ -98,6 +98,7 @@ export const READ_CONFIG_KEYS = [
98
98
  'cache',
99
99
  'deviceProps',
100
100
  'wantedPreKeyCount',
101
+ 'dangerSkipCertChainVerify',
101
102
  'emitOwnEvents',
102
103
  'shouldIgnoreJid',
103
104
  'defaultQueryTimeoutMs',
@@ -93,6 +93,14 @@ export type SocketConfig = {
93
93
  * generated and encoded in one shot). Must be set before connecting.
94
94
  */
95
95
  wantedPreKeyCount?: number;
96
+ /**
97
+ * Testing-only bypass for the Noise server-cert chain check, for mock
98
+ * servers that cannot sign a chain rooted in WhatsApp's issuer. Strict by
99
+ * default: absent, null and false all verify, and only a literal `true`
100
+ * opts in — the bridge rejects any other truthy value at construction
101
+ * rather than treating it as opt-in. Never set this outside tests.
102
+ */
103
+ dangerSkipCertChainVerify?: boolean;
96
104
  /** @deprecated QR timeout is handled by the bridge connection state machine. */
97
105
  qrTimeout?: number;
98
106
  /** Maximum retry count. */
@@ -1,13 +1,84 @@
1
1
  import type { AuthenticationState } from '../Types/index.js';
2
+ /**
3
+ * Pure classifier for directory-barrier failures: does this error mean
4
+ * "this platform cannot sync directory handles" (degrade to process-crash
5
+ * atomicity) or "the durability barrier did not hold" (propagate)?
6
+ *
7
+ * - `ENOSYS` / `ENOTSUP` on any platform: the operation is not
8
+ * implemented — genuinely unsupported, safe to degrade.
9
+ * - `EINVAL` / `EPERM` / `EISDIR` on `win32` only: Windows directory
10
+ * handles reject open-for-read and FlushFileBuffers with these codes,
11
+ * so they are the documented platform fallback there. On Linux/macOS
12
+ * the same codes from a freshly opened directory handle mean something
13
+ * is genuinely wrong and they propagate.
14
+ * - Everything else propagates everywhere: `EIO`, `ENOSPC`, `EROFS`,
15
+ * `EACCES`, `ENOENT`, and notably `EBADF` (a bad handle is a real bug,
16
+ * never evidence of an unsupported platform).
17
+ *
18
+ * `platform` defaults to the running platform; tests pass explicit values
19
+ * to cover the matrix deterministically on any OS.
20
+ */
21
+ export declare const isUnsupportedDirSync: (e: unknown, platform?: NodeJS.Platform) => boolean;
22
+ type BridgeStoreFileHandle = {
23
+ writeFile(value: Uint8Array): Promise<void>;
24
+ sync(): Promise<void>;
25
+ close(): Promise<void>;
26
+ };
27
+ type BridgeStoreFileIO = {
28
+ writeTmp?(tmpPath: string, value: Uint8Array): Promise<void>;
29
+ publishTmp?(tmpPath: string, finalPath: string): Promise<void>;
30
+ syncDir?(dir: string): Promise<void>;
31
+ openTmp?(tmpPath: string): Promise<BridgeStoreFileHandle>;
32
+ };
33
+ type BridgeStoreOptions = {
34
+ io?: BridgeStoreFileIO;
35
+ };
2
36
  /**
3
37
  * Creates a file-based store for the WASM bridge.
4
38
  *
5
39
  * Each (store, key) pair maps to a file: `<folder>/<store>-<key>.bin`
6
40
  *
7
- * Uses a write-through in-memory cache to avoid redundant disk reads.
8
- * Writes go to both cache and disk. Reads hit cache first, disk on miss.
41
+ * Durability model:
42
+ * - Caller buffers are copied synchronously at admission (`set`/`setMany`
43
+ * copy before queueing), so mutating a buffer after the call — even
44
+ * before awaiting it — can never change what gets persisted.
45
+ * - Critical stores write through `durableWrite` before `set`/`setMany`
46
+ * resolve. The in-memory map only records bytes AFTER the full barrier
47
+ * (write + fsync + atomic rename + directory sync) succeeds, so an
48
+ * identical retry following a failure is never skipped and always
49
+ * re-attempts the write.
50
+ * - If the post-rename barrier fails, the key is marked uncertain: prior
51
+ * durable knowledge is discarded, reads serve best-available bytes
52
+ * without re-certifying them, and no identical set is skipped until a
53
+ * later operation completes the full barrier for that key.
54
+ * - Non-critical stores are debounced (50ms coalescing) and readable
55
+ * immediately (read-your-write), but such reads are NOT durable until
56
+ * `flush()` succeeds. A failed flush keeps the pending entry and throws,
57
+ * so the next `flush()` retries the same bytes.
58
+ * - `flush()` first waits for every operation admitted before it
59
+ * (barrier), then drains the pending writes those operations produced.
60
+ * Failures observed by the barrier propagate — a failed admitted write
61
+ * fails the flush — but only operations outstanding during that flush
62
+ * are reported, so history never poisons later flushes. A drain pass
63
+ * with any failure stops at that pass and leaves the failed entries
64
+ * for the next explicit flush. `flush()` never reports quiescence
65
+ * while prior admitted work is still running.
66
+ * - All operations on one key (set, delete, flush, concurrent batches) run
67
+ * through a per-key chain, so a stale failure can never erase newer
68
+ * state and an in-flight write can never resurrect a deleted key.
69
+ * - A failed delete restores the preceding pending/durable state, so an
70
+ * acknowledged value stays readable and flushable; only a successful
71
+ * delete (unlink + directory barrier) clears it. A delete retried while
72
+ * the key is uncertain re-runs the directory barrier instead of
73
+ * swallowing the uncertainty as idempotent absence — except that a
74
+ * directory removed externally surfaces ENOENT rather than success.
75
+ * - Every byte array handed back to callers is a copy.
9
76
  *
10
77
  * @param folder Directory to store bridge state files
78
+ * @param options Optional per-store file-I/O steps. Test/fault-injection
79
+ * seam only: the default implementation is the sole provider of the
80
+ * durability contract above.
11
81
  */
12
- export declare function useBridgeStore(folder: string): Promise<NonNullable<AuthenticationState['store']>>;
82
+ export declare function useBridgeStore(folder: string, options?: BridgeStoreOptions): Promise<NonNullable<AuthenticationState['store']>>;
83
+ export {};
13
84
  //# sourceMappingURL=use-bridge-store.d.ts.map