@demlik/tea 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/agent/index.d.ts +36 -23
- package/dist/agent/index.js +12 -10
- package/dist/{chunk-UHAQ4PI6.js → chunk-2A5XTKQQ.js} +205 -59
- package/dist/chunk-2A5XTKQQ.js.map +1 -0
- package/dist/{chunk-JAKM53JF.js → chunk-7K7SEYCH.js} +16 -15
- package/dist/chunk-7K7SEYCH.js.map +1 -0
- package/dist/{chunk-RKVUVALV.js → chunk-A3QPMX5M.js} +31 -9
- package/dist/chunk-A3QPMX5M.js.map +1 -0
- package/dist/{chunk-DVNXEB7V.js → chunk-BVFS3ICC.js} +3 -3
- package/dist/chunk-BVFS3ICC.js.map +1 -0
- package/dist/{chunk-FTCYY2DO.js → chunk-CPE5ZQXQ.js} +3 -3
- package/dist/{chunk-FTCYY2DO.js.map → chunk-CPE5ZQXQ.js.map} +1 -1
- package/dist/chunk-DEAPNMKO.js +11 -0
- package/dist/chunk-DEAPNMKO.js.map +1 -0
- package/dist/{chunk-XWIE5V7R.js → chunk-ETRNSJCR.js} +4 -4
- package/dist/{chunk-XWIE5V7R.js.map → chunk-ETRNSJCR.js.map} +1 -1
- package/dist/chunk-FXWDDNGD.js +98 -0
- package/dist/chunk-FXWDDNGD.js.map +1 -0
- package/dist/chunk-GFGOQIEC.js +10 -0
- package/dist/chunk-GFGOQIEC.js.map +1 -0
- package/dist/{chunk-2R62Q5UK.js → chunk-IAHEYPYJ.js} +3 -3
- package/dist/{chunk-2R62Q5UK.js.map → chunk-IAHEYPYJ.js.map} +1 -1
- package/dist/{chunk-NGYFBD5G.js → chunk-J5JNLXO7.js} +3 -3
- package/dist/{chunk-NGYFBD5G.js.map → chunk-J5JNLXO7.js.map} +1 -1
- package/dist/{chunk-72CN3VPY.js → chunk-NFOGYDRS.js} +5 -5
- package/dist/{chunk-72CN3VPY.js.map → chunk-NFOGYDRS.js.map} +1 -1
- package/dist/{chunk-A73IJ27X.js → chunk-R2YNDRGL.js} +3 -3
- package/dist/{chunk-A73IJ27X.js.map → chunk-R2YNDRGL.js.map} +1 -1
- package/dist/{chunk-5HVKDHY6.js → chunk-R5NXOPL2.js} +3 -3
- package/dist/{chunk-5HVKDHY6.js.map → chunk-R5NXOPL2.js.map} +1 -1
- package/dist/chunk-RMBZ7YH7.js +52 -0
- package/dist/chunk-RMBZ7YH7.js.map +1 -0
- package/dist/{chunk-AIXC33SE.js → chunk-SHXUSTHM.js} +62 -15
- package/dist/chunk-SHXUSTHM.js.map +1 -0
- package/dist/{chunk-TAKPC6SD.js → chunk-UGTC33VH.js} +4 -4
- package/dist/{chunk-TAKPC6SD.js.map → chunk-UGTC33VH.js.map} +1 -1
- package/dist/{chunk-EA47MI5Q.js → chunk-VH5MYJ2K.js} +4 -4
- package/dist/{chunk-EA47MI5Q.js.map → chunk-VH5MYJ2K.js.map} +1 -1
- package/dist/{chunk-2774I242.js → chunk-WZEDIUWR.js} +21 -20
- package/dist/chunk-WZEDIUWR.js.map +1 -0
- package/dist/{chunk-TGJLXZAU.js → chunk-ZRRET3RP.js} +11 -11
- package/dist/chunk-ZRRET3RP.js.map +1 -0
- package/dist/{core-CtvAyPpH.d.ts → core-DNDAEhO2.d.ts} +9 -12
- package/dist/devtools/index.d.ts +4 -4
- package/dist/do/index.d.ts +103 -38
- package/dist/do/index.js +40 -21
- package/dist/do/index.js.map +1 -1
- package/dist/{drive-loop-Dw1sGSsW.d.ts → drive-loop-DlOEv49c.d.ts} +2 -2
- package/dist/{durable-effects-Ump5lSdO.d.ts → durable-effects-wLWEdy9H.d.ts} +1 -1
- package/dist/effect/index.d.ts +121 -28
- package/dist/effect/index.js +45 -31
- package/dist/effect/index.js.map +1 -1
- package/dist/extension/index.d.ts +3 -3
- package/dist/extension/index.js +2 -2
- package/dist/extension/index.js.map +1 -1
- package/dist/flow/index.d.ts +17 -13
- package/dist/flow/index.js +15 -12
- package/dist/flow/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +3 -3
- package/dist/idempotency/index.js +3 -3
- package/dist/{index-CrcoVfd_.d.ts → index-9P1PaXIV.d.ts} +2 -2
- package/dist/{index-D3NY3VCN.d.ts → index-BBLI2WWN.d.ts} +1 -1
- package/dist/{index-Dl71V7kt.d.ts → index-C1WhBZ6a.d.ts} +4 -5
- package/dist/{index-CKtEKGyp.d.ts → index-C92SgNWz.d.ts} +3 -3
- package/dist/{index-CZZ3HI-Z.d.ts → index-CAsUE_d1.d.ts} +4 -4
- package/dist/{index-EIgfl83J.d.ts → index-CO34esWT.d.ts} +2 -2
- package/dist/{index-Bk2hBUdK.d.ts → index-COeX9Bmz.d.ts} +11 -9
- package/dist/{index-CVU7SK3a.d.ts → index-Cg0QXnzf.d.ts} +2 -2
- package/dist/index.d.ts +9 -9
- package/dist/index.js +2 -2
- package/dist/jev/index.d.ts +15 -15
- package/dist/jev/index.js +5 -5
- package/dist/jev/index.js.map +1 -1
- package/dist/laws-DTtCDDlU.d.ts +135 -0
- package/dist/loop-DReKGV1X.d.ts +70 -0
- package/dist/{machine-Dtre2TmN.d.ts → machine-C0WhOAyB.d.ts} +7 -7
- package/dist/machine-viz/index.d.ts +1 -1
- package/dist/machine-viz/index.js +2 -2
- package/dist/mem/index.d.ts +4 -3
- package/dist/mem/index.js +2 -2
- package/dist/mem/index.js.map +1 -1
- package/dist/node/index.d.ts +57 -9
- package/dist/node/index.js +175 -68
- package/dist/node/index.js.map +1 -1
- package/dist/otel/index.d.ts +28 -12
- package/dist/otel/index.js +25 -21
- package/dist/otel/index.js.map +1 -1
- package/dist/paginate/index.d.ts +4 -4
- package/dist/paginate/index.js +5 -5
- package/dist/parity/index.d.ts +11 -15
- package/dist/parity/index.js +4 -9
- package/dist/parity/index.js.map +1 -1
- package/dist/pbt/index.d.ts +279 -67
- package/dist/pbt/index.js +299 -58
- package/dist/pbt/index.js.map +1 -1
- package/dist/persistence/index.d.ts +10 -12
- package/dist/persistence/index.js +4 -4
- package/dist/promise/index.d.ts +118 -21
- package/dist/promise/index.js +6 -4
- package/dist/react/index.d.ts +34 -21
- package/dist/react/index.js +271 -42
- package/dist/react/index.js.map +1 -1
- package/dist/resilience/index.d.ts +6 -6
- package/dist/resilience/index.js +5 -5
- package/dist/{run-fiBe7pOG.d.ts → run-Cnw4qXj2.d.ts} +105 -23
- package/dist/{runtime-types-DEluQgGO.d.ts → runtime-types-CP-ykwgf.d.ts} +157 -29
- package/dist/testing/effect/index.d.ts +162 -9
- package/dist/testing/effect/index.js +110 -10
- package/dist/testing/effect/index.js.map +1 -1
- package/dist/testing/index.d.ts +6 -8
- package/dist/testing/index.js +4 -24
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/promise/index.d.ts +4 -4
- package/dist/testing/promise/index.js +5 -5
- package/dist/timing/index.d.ts +4 -4
- package/dist/timing/index.js +4 -4
- package/dist/{types-CXNzlbAg.d.ts → types-S53WDy_h.d.ts} +1 -1
- package/dist/work-queue/index.d.ts +4 -4
- package/dist/work-queue/index.js +2 -2
- package/package.json +19 -17
- package/dist/chunk-2774I242.js.map +0 -1
- package/dist/chunk-AIXC33SE.js.map +0 -1
- package/dist/chunk-DVNXEB7V.js.map +0 -1
- package/dist/chunk-HX3C6LDK.js +0 -34
- package/dist/chunk-HX3C6LDK.js.map +0 -1
- package/dist/chunk-JAKM53JF.js.map +0 -1
- package/dist/chunk-RKVUVALV.js.map +0 -1
- package/dist/chunk-TGJLXZAU.js.map +0 -1
- package/dist/chunk-UHAQ4PI6.js.map +0 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The core loop both engines run (#280, spike #264). It does five things and
|
|
3
|
+
* nothing else: it keeps one serial tail, folds a Msg into a transition, saves
|
|
4
|
+
* before any effect, reconciles the running Subs, and runs the transition's
|
|
5
|
+
* Cmds, dispatching what they return. It has no special case for any built-in.
|
|
6
|
+
*
|
|
7
|
+
* One loop, not one per engine (#283). The loop calls a handler and a runner
|
|
8
|
+
* through erased signatures and awaits a Promise either way, so an engine's
|
|
9
|
+
* own types never pass through it. The Effect engine turns each handler and
|
|
10
|
+
* runner into a fiber at its own edge, run with the services the caller
|
|
11
|
+
* provided, so Effect services (`R`) reach every handler without the loop
|
|
12
|
+
* carrying a type slot for them — the gap spike #260's generic loop left open.
|
|
13
|
+
* Sharing the loop is also what makes the two engines' traces identical: the
|
|
14
|
+
* fold, the save order and the built-ins are the same code.
|
|
15
|
+
*
|
|
16
|
+
* Everything else `run` offers is an {@link Extension} built on ten points:
|
|
17
|
+
*
|
|
18
|
+
* 1. `update` — middleware over the fold. Returning `null` means "no
|
|
19
|
+
* transition": no save, no effects, no commit.
|
|
20
|
+
* 2. `interpret` — middleware over one Cmd's handler call; it maps what the
|
|
21
|
+
* handler returned to the Msgs to dispatch, if any.
|
|
22
|
+
* 3. `store` — a wrapper over the store the caller handed `run`.
|
|
23
|
+
* 4. `installed` — called once a transition's State is past its save and its
|
|
24
|
+
* Subs are reconciled, before its first Cmd's handler starts.
|
|
25
|
+
* This is Elm's `view`: it never waits on a Cmd (#695).
|
|
26
|
+
* 5. `commit` — called after a transition's effects, with the Msg and the
|
|
27
|
+
* committed State, even when a Cmd handler threw. Boot
|
|
28
|
+
* passes `undefined` for the Msg.
|
|
29
|
+
* 6. `saveFailed` — called when a transition's save rejects, before that
|
|
30
|
+
* rejection reaches the dispatch caller.
|
|
31
|
+
* 7. `subFailed` — called when a Sub's failure stops the run, before the
|
|
32
|
+
* engine is told to stop it.
|
|
33
|
+
* 8. `halted` — called when an extension halts the run with an error.
|
|
34
|
+
* 9. `drained` — called once no Msg can fold any more, so the State is the
|
|
35
|
+
* run's last: `stop()` has finished with the queue, the
|
|
36
|
+
* host's or the one a failure started.
|
|
37
|
+
* 10. `ctx` — keys added to the ctx every Cmd handler receives, so a
|
|
38
|
+
* handler that runs another handler inside its own can reach
|
|
39
|
+
* the same machinery the loop applies to it.
|
|
40
|
+
*
|
|
41
|
+
* An extension may also add methods to the run handle (`handle`). Array order
|
|
42
|
+
* is nesting order: the first extension's middleware is the outermost, and
|
|
43
|
+
* `installed` and commit callbacks run first to last. `builtinExtensions`
|
|
44
|
+
* (`./builtins`) fixes that order for every engine; it is not a user-facing
|
|
45
|
+
* plugin API (#268).
|
|
46
|
+
*
|
|
47
|
+
* `stop()` is the core's own lifecycle, because the gate it drives is what every
|
|
48
|
+
* dispatch passes through: open → draining → closed, or → abandoned when
|
|
49
|
+
* `stop()` gives up on the queue (ADR 0025), never backwards.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
declare const post: unique symbol;
|
|
53
|
+
/**
|
|
54
|
+
* What `tell` answers, at once, about the Msg it was handed. It says where the
|
|
55
|
+
* Msg stands at that moment and nothing about how it ends.
|
|
56
|
+
*
|
|
57
|
+
* - `"queued"`: the Msg is on the receiver's line. It has not been handled.
|
|
58
|
+
* - `"stopped"`: the receiver is stopping or has stopped, so the Msg is not on
|
|
59
|
+
* its line and never will be.
|
|
60
|
+
* - `"no_cell"`: the receiver's current State has no cell for the Msg, so the
|
|
61
|
+
* Msg is not on its line. That reads the State as it is now, not the State
|
|
62
|
+
* the Msgs already in line will leave.
|
|
63
|
+
*/
|
|
64
|
+
type TellAnswer = "queued" | "stopped" | "no_cell";
|
|
65
|
+
/** A run handle `tell` can reach the line of. */
|
|
66
|
+
interface Postable<S, M> {
|
|
67
|
+
readonly [post]: (msg: M, takes: (state: S) => boolean) => TellAnswer;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type { Postable as P, TellAnswer as T };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { C as Cmd,
|
|
2
|
-
import { b as MonitoredRunState, R as RunFailure, M as MonitoredRunCmd } from './index-
|
|
3
|
-
import { R as RunCmd, S as SucceedMsg, F as FailMsg, b as ResilientState, r as RunCmdDef } from './index-
|
|
4
|
-
import { D as DeadlineExceeded, a as DeadlineSub, b as DeadlinesSub } from './index-
|
|
1
|
+
import { C as Cmd, s as Tagged, I as Interpret, A as AnyCmdDef, a as CmdOf, b as CmdDef, h as Settled, t as OkOf, u as ErrOf, o as MalformedResult, O as Outcome, v as CmdValue, H as HandlerCtx, f as TaggedError, M as Machine, S as Subscribe } from './core-DNDAEhO2.js';
|
|
2
|
+
import { b as MonitoredRunState, R as RunFailure, M as MonitoredRunCmd } from './index-CAsUE_d1.js';
|
|
3
|
+
import { R as RunCmd, S as SucceedMsg, F as FailMsg, b as ResilientState, r as RunCmdDef } from './index-C92SgNWz.js';
|
|
4
|
+
import { D as DeadlineExceeded, a as DeadlineSub, b as DeadlinesSub } from './index-Cg0QXnzf.js';
|
|
5
5
|
import { M as MsgType, a as MsgTypeValue } from './protocol-ClRZBhHQ.js';
|
|
6
|
-
import { F as FanOutState } from './index-
|
|
6
|
+
import { F as FanOutState } from './index-9P1PaXIV.js';
|
|
7
7
|
import { RetryPolicy, AnyRetryPolicy } from './retry-backoff/index.js';
|
|
8
8
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
9
9
|
|
|
@@ -1197,7 +1197,7 @@ declare function omitMedia(parts: readonly ContentPart[]): readonly ContentPart[
|
|
|
1197
1197
|
* @demlik/tea/agent — the one construction every tool definition goes through.
|
|
1198
1198
|
*
|
|
1199
1199
|
* NOT part of the public `./agent` barrel. `tool()` and `agentTool()` are two
|
|
1200
|
-
*
|
|
1200
|
+
* constructors of the same `ToolDef`: `tool()` hands its handler the parsed `args`,
|
|
1201
1201
|
* and `agentTool()` needs the whole Cmd — `callId` included — because the call's
|
|
1202
1202
|
* identity is what keys the child run. Both are built here, so the reserved-name
|
|
1203
1203
|
* refusal, the `thrown` channel and the resilience knob have one definition.
|
|
@@ -1227,7 +1227,7 @@ declare const SNAPSHOT_WRITE_TYPE = "snapshot_write";
|
|
|
1227
1227
|
* `snapshot_write`, which `toMachine` merges under the consumer's key.
|
|
1228
1228
|
*/
|
|
1229
1229
|
type ReservedToolName = MsgTypeValue | SettlePrefixOf<MsgTypeValue> | typeof REJECTED_TYPE | typeof SNAPSHOT_WRITE_TYPE;
|
|
1230
|
-
/** Whether `name` is one a tool
|
|
1230
|
+
/** Whether `name` is one a tool constructor refuses — the set `ReservedToolName` types. */
|
|
1231
1231
|
declare function isReservedToolName(name: string): name is ReservedToolName;
|
|
1232
1232
|
/**
|
|
1233
1233
|
* Widens to `unknown` for a name outside the reserved set and to `never`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import '../chunk-
|
|
2
|
-
import { formOf, subEntriesOf, desiredSub } from '../chunk-
|
|
1
|
+
import '../chunk-BVFS3ICC.js';
|
|
2
|
+
import { formOf, subEntriesOf, desiredSub } from '../chunk-A3QPMX5M.js';
|
|
3
3
|
|
|
4
4
|
// src/machine-viz/mermaid-id.ts
|
|
5
5
|
function safeId(raw) {
|
package/dist/mem/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { M as Migrated, D as DeletableStore, F as FencedStore } from '../runtime-types-
|
|
2
|
-
import '../core-
|
|
1
|
+
import { M as Migrated, D as DeletableStore, F as FencedStore } from '../runtime-types-CP-ykwgf.js';
|
|
2
|
+
import '../core-DNDAEhO2.js';
|
|
3
3
|
import '@standard-schema/spec';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -39,7 +39,8 @@ interface MemoryStoreOptions {
|
|
|
39
39
|
* store is a `FencedStore<S>` whose version is an in-process counter — honest
|
|
40
40
|
* for this adapter, since the cell it guards is in this process too. Two
|
|
41
41
|
* `run`s handed the SAME `memoryStore` are the case it catches, and it is the
|
|
42
|
-
* cheapest way to exercise the fenced path in a test.
|
|
42
|
+
* cheapest way to exercise the fenced path in a test. A run on a fenced store
|
|
43
|
+
* must be given an `onError` (see `StoreAndOnError`).
|
|
43
44
|
*/
|
|
44
45
|
readonly fenced?: true;
|
|
45
46
|
}
|
package/dist/mem/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import '../chunk-
|
|
2
|
-
import { StoreConflictError } from '../chunk-
|
|
1
|
+
import '../chunk-BVFS3ICC.js';
|
|
2
|
+
import { StoreConflictError } from '../chunk-A3QPMX5M.js';
|
|
3
3
|
|
|
4
4
|
// src/mem/index.ts
|
|
5
5
|
function memoryStore(initial, parse = (raw) => raw, options = {}) {
|
package/dist/mem/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/mem/index.ts"],"names":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"sources":["../../src/mem/index.ts"],"names":[],"mappings":";;;;AAuEO,SAAS,WAAA,CACd,SACA,KAAA,GAAuC,CAAC,QAAQ,GAAA,EAChD,OAAA,GAA8B,EAAC,EAC2B;AAG1D,EAAA,IAAI,OAAiB,OAAA,IAAW,IAAA;AAGhC,EAAA,IAAI,OAAA,GAAU,CAAA;AAEd,EAAA,MAAM,IAAA,GAA0B;AAAA,IAC9B,MAAM,IAAA,GAAyB;AAC7B,MAAA,OAAO,IAAA;AAAA,IACT,CAAA;AAAA,IACA,MAAM,KAAK,KAAA,EAAyB;AAClC,MAAA,IAAA,GAAO,KAAA;AACP,MAAA,OAAA,IAAW,CAAA;AAAA,IACb,CAAA;AAAA,IACA,QAAQ,GAAA,EAA2B;AACjC,MAAA,OAAO,MAAM,GAAG,CAAA;AAAA,IAClB,CAAA;AAAA,IACA,MAAM,MAAA,GAAwB;AAG5B,MAAA,IAAA,GAAO,IAAA;AACP,MAAA,OAAA,GAAU,CAAA;AAAA,IACZ;AAAA,GACF;AACA,EAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,IAAA,EAAM,OAAO,IAAA;AAEpC,EAAA,OAAO;AAAA,IACL,GAAG,IAAA;AAAA,IACH,MAAA,EAAQ,IAAA;AAAA,IACR,MAAM,UAAA,GAAyD;AAC7D,MAAA,OAAO,EAAE,GAAA,EAAK,IAAA,EAAM,OAAA,EAAQ;AAAA,IAC9B,CAAA;AAAA,IACA,MAAM,UAAA,CAAW,KAAA,EAAU,eAAA,EAA0C;AAInE,MAAA,IAAI,YAAY,eAAA,EAAiB;AAC/B,QAAA,MAAM,IAAI,kBAAA,CAAmB,eAAA,EAAiB,OAAO,CAAA;AAAA,MACvD;AACA,MAAA,IAAA,GAAO,KAAA;AACP,MAAA,OAAA,IAAW,CAAA;AACX,MAAA,OAAO,OAAA;AAAA,IACT;AAAA,GACF;AACF","file":"index.js","sourcesContent":["/**\n * @packageDocumentation\n * @demlik/tea/mem — in-memory `Store<S>` adapter for `@demlik/tea`.\n *\n * The standard test adapter — used everywhere a Worker or DO would be\n * overkill. Mirrors the source-only convention from `packages/widget-engine`\n * and `packages/tea` (no build step).\n *\n * Reference semantics, NOT deep-clone semantics:\n * - `save(state)` stores the reference; subsequent mutations to that\n * reference by the caller are observable through later `load()` calls.\n * - `load()` returns the stored reference; mutations made through it are\n * observable on the next `load()`.\n *\n * This matches what a real KV/DO `get()` would NOT do (those serialize +\n * deserialize), but it also matches what an in-memory cache obviously does.\n * Adding a deep clone here would lie about the boundary and impose a JSON\n * round-trip on every test. Tests that need fresh copies should clone at the\n * call site.\n *\n * Boundary parse (invariant 8): `Store<S>` requires `migrate(raw)`. For\n * `memoryStore` we ship a permissive identity-default — `(raw) => raw as\n * S | null` — because this adapter is a degenerate same-process boundary:\n * there is no serialization, the reference we read is structurally the\n * reference we wrote. The `as` cast here is the documented exception to the\n * invariant-8 no-`as` rule, scoped to the parameter default. Callers that\n * want strict parsing (e.g. a test that hand-seeds the cell from outside)\n * may pass an explicit `parse` to override.\n */\n\nimport type { DeletableStore, FencedStore, Migrated } from \"../index\";\nimport { StoreConflictError } from \"../index\";\n\n/** Options for {@link memoryStore}. */\nexport interface MemoryStoreOptions {\n /**\n * Refuse a second live writer (#143). With `{ fenced: true }` the returned\n * store is a `FencedStore<S>` whose version is an in-process counter — honest\n * for this adapter, since the cell it guards is in this process too. Two\n * `run`s handed the SAME `memoryStore` are the case it catches, and it is the\n * cheapest way to exercise the fenced path in a test. A run on a fenced store\n * must be given an `onError` (see `StoreAndOnError`).\n */\n readonly fenced?: true;\n}\n\n/**\n * Build an in-memory `Store<S>`.\n *\n * @param initial - optional seed value. `undefined` and explicit `null` both\n * mean \"no seed\"; `load()` resolves with `null` until the first `save()`.\n * A concrete `S` seed is returned by `load()` until overwritten.\n * @param parse - optional boundary parse for the `Store<S>.migrate` contract.\n * Defaults to identity (`(raw) => raw as S | null`) — honest for this\n * adapter's degenerate same-process boundary. Pass an explicit parser when\n * the test wants to reject seeded shapes that don't match `S`.\n */\nexport function memoryStore<S>(\n initial?: S | null,\n parse?: (raw: unknown) => Migrated<S>,\n): DeletableStore<S>;\nexport function memoryStore<S>(\n initial: S | null | undefined,\n parse: ((raw: unknown) => Migrated<S>) | undefined,\n options: MemoryStoreOptions & { readonly fenced: true },\n): FencedStore<S> & DeletableStore<S>;\nexport function memoryStore<S>(\n initial?: S | null,\n parse?: (raw: unknown) => Migrated<S>,\n options?: MemoryStoreOptions,\n): DeletableStore<S> | (FencedStore<S> & DeletableStore<S>);\nexport function memoryStore<S>(\n initial?: S | null,\n parse: (raw: unknown) => Migrated<S> = (raw) => raw as S | null,\n options: MemoryStoreOptions = {},\n): DeletableStore<S> | (FencedStore<S> & DeletableStore<S>) {\n // Single internal cell — `undefined` and `null` collapse to one\n // representation so the load site is branchless.\n let cell: S | null = initial ?? null;\n // The version the cell was last written at. A never-written cell reads `0`,\n // matching `fileStore`'s absent stamp.\n let version = 0;\n\n const base: DeletableStore<S> = {\n async load(): Promise<unknown> {\n return cell;\n },\n async save(state: S): Promise<void> {\n cell = state;\n version += 1;\n },\n migrate(raw: unknown): Migrated<S> {\n return parse(raw);\n },\n async delete(): Promise<void> {\n // Back to never-written: an empty cell at version `0`. The seed is gone\n // too — `load()` answers what an unseeded store answers.\n cell = null;\n version = 0;\n },\n };\n if (options.fenced !== true) return base;\n\n return {\n ...base,\n fenced: true,\n async loadFenced(): Promise<{ raw: unknown; version: number }> {\n return { raw: cell, version };\n },\n async saveFenced(state: S, expectedVersion: number): Promise<number> {\n // No lock: the whole store is one process's memory, and this body has no\n // await before the compare, so nothing can interleave between the check\n // and the write.\n if (version !== expectedVersion) {\n throw new StoreConflictError(expectedVersion, version);\n }\n cell = state;\n version += 1;\n return version;\n },\n };\n}\n"]}
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
|
-
import { M as Migrated, D as DeletableStore, F as FencedStore } from '../runtime-types-
|
|
3
|
-
import {
|
|
2
|
+
import { M as Migrated, D as DeletableStore, F as FencedStore } from '../runtime-types-CP-ykwgf.js';
|
|
3
|
+
import { c as Sub, D as Dispose } from '../core-DNDAEhO2.js';
|
|
4
4
|
import '@standard-schema/spec';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -79,6 +79,30 @@ interface Journal<R> {
|
|
|
79
79
|
withLock<A>(stream: string, fx: () => Promise<A>): Promise<A>;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Why a writer could not take a lock:
|
|
84
|
+
*
|
|
85
|
+
* - `another-host`: the lock names another host, which may still be writing.
|
|
86
|
+
* - `live-holder`: its process is alive on this machine, or its old-format id
|
|
87
|
+
* names a live process here.
|
|
88
|
+
* - `unwritten`: the lock is empty and too young to count as a crash.
|
|
89
|
+
* - `unreadable`: its contents are not a lock this version can read.
|
|
90
|
+
* - `contended`: no one holder was to blame; other writers kept taking it.
|
|
91
|
+
*/
|
|
92
|
+
type FileLockBlock = "another-host" | "live-holder" | "unwritten" | "unreadable" | "contended";
|
|
93
|
+
/**
|
|
94
|
+
* A write gave up waiting for a lock file. `lockPath` names the file to clear
|
|
95
|
+
* and `blockedBy` says why it could not be taken. The write did not happen.
|
|
96
|
+
*/
|
|
97
|
+
declare class FileLockTimeoutError extends Error {
|
|
98
|
+
readonly lockPath: string;
|
|
99
|
+
readonly waitedMs: number;
|
|
100
|
+
readonly blockedBy: FileLockBlock;
|
|
101
|
+
readonly name = "FileLockTimeoutError";
|
|
102
|
+
readonly _tag: "file_lock_timeout";
|
|
103
|
+
constructor(lockPath: string, waitedMs: number, blockedBy: FileLockBlock);
|
|
104
|
+
}
|
|
105
|
+
|
|
82
106
|
/**
|
|
83
107
|
* @packageDocumentation
|
|
84
108
|
* @demlik/tea/node — Node host adapter for `@demlik/tea`.
|
|
@@ -158,6 +182,25 @@ interface FileStoreOptions {
|
|
|
158
182
|
* that started from the same version is refused with a `StoreConflictError`
|
|
159
183
|
* at its next save. Omit it and the store is exactly as it was — the last
|
|
160
184
|
* writer wins, and single-writer is the caller's precondition to keep.
|
|
185
|
+
*
|
|
186
|
+
* A run on a fenced store must be given an `onError`, or it does not compile
|
|
187
|
+
* (see `StoreAndOnError`). A lost fence can surface with no caller to reject
|
|
188
|
+
* at, under `"stop-save"` or `"follow-up"`, and the default sink would rethrow
|
|
189
|
+
* it and end the process. Treat a `StoreConflictError` there as this run having
|
|
190
|
+
* moved to another process: stop that run, and leave the others alone.
|
|
191
|
+
*
|
|
192
|
+
* Each save and `delete()` takes a lock file, `<path>.fence.lock`, and a
|
|
193
|
+
* writer that dies holding it leaves it behind. The next write steals a lock
|
|
194
|
+
* whose holder is proven gone: an empty one older than a short grace period,
|
|
195
|
+
* one from an earlier boot of this machine, or one whose process is dead.
|
|
196
|
+
*
|
|
197
|
+
* Any other lock is waited on for up to 10 seconds, and then the write rejects
|
|
198
|
+
* with a {@link FileLockTimeoutError} naming the file. That includes a lock
|
|
199
|
+
* from another host, which is never stolen because that host may be live on a
|
|
200
|
+
* shared volume. So a folder restored onto a new machine can need one clear
|
|
201
|
+
* step: once the old machine is known to be gone, delete the file. Hosts are
|
|
202
|
+
* told apart by `os.hostname()`, so give two live hosts on one volume two
|
|
203
|
+
* names.
|
|
161
204
|
*/
|
|
162
205
|
readonly fenced?: true;
|
|
163
206
|
}
|
|
@@ -176,12 +219,17 @@ interface FileStoreOptions {
|
|
|
176
219
|
* O(n) in the stream's length; a durable record log a machine replays is read
|
|
177
220
|
* whole anyway, so the atomic-rewrite guarantee is worth the cost.
|
|
178
221
|
*
|
|
179
|
-
* The ordering key is taken under a `wx` lock file (`<file>.lock`)
|
|
180
|
-
* writer's PID: a second writer racing the same stream blocks on
|
|
181
|
-
* `seq` is a single total order across processes.
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
222
|
+
* The ordering key is taken under a `wx` lock file (`<file>.lock`) naming the
|
|
223
|
+
* writer's host, boot and PID: a second writer racing the same stream blocks on
|
|
224
|
+
* the lock, so `seq` is a single total order across processes. This is the lock
|
|
225
|
+
* phoenix spike #6673 proved: its only job is an honest order key, not the win.
|
|
226
|
+
*
|
|
227
|
+
* A leftover lock behaves as it does on a fenced {@link fileStore}: one whose
|
|
228
|
+
* holder is proven gone is stolen, so a crash on this machine does not wedge the
|
|
229
|
+
* stream. Any other lock, including one from another host, is waited on for up
|
|
230
|
+
* to 10 seconds, and then the append rejects with a
|
|
231
|
+
* {@link FileLockTimeoutError} naming the file. See
|
|
232
|
+
* {@link FileStoreOptions.fenced} for the cases and the clear step.
|
|
185
233
|
*
|
|
186
234
|
* `changes` fires for appends made through THIS instance; a cross-process
|
|
187
235
|
* append lands on disk but is not broadcast here (that is a polling concern the
|
|
@@ -315,4 +363,4 @@ interface NodeSubscribeWithWs<M, Ctx extends NodeSubscribeCtx> extends NodeSubsc
|
|
|
315
363
|
declare function nodeSubscribe<M>(): NodeSubscribeBase<M>;
|
|
316
364
|
declare function nodeSubscribe<M, Ctx extends NodeSubscribeCtx>(opts: NodeSubscribeOpts<M>): NodeSubscribeWithWs<M, Ctx>;
|
|
317
365
|
|
|
318
|
-
export { type FileStoreOptions, type NodeSignalDeps, type NodeSignalSub, type NodeSub, type NodeSubscribeBase, type NodeSubscribeCtx, type NodeSubscribeOpts, type NodeSubscribeWithWs, type NodeTimerDeps, type NodeTimerSub, type NodeWsDeps, type NodeWsHandlers, type NodeWsRegistry, type NodeWsSub, fileJournal, fileStore, nodeSubscribe, sendToWebSocket };
|
|
366
|
+
export { type FileLockBlock, FileLockTimeoutError, type FileStoreOptions, type NodeSignalDeps, type NodeSignalSub, type NodeSub, type NodeSubscribeBase, type NodeSubscribeCtx, type NodeSubscribeOpts, type NodeSubscribeWithWs, type NodeTimerDeps, type NodeTimerSub, type NodeWsDeps, type NodeWsHandlers, type NodeWsRegistry, type NodeWsSub, fileJournal, fileStore, nodeSubscribe, sendToWebSocket };
|
package/dist/node/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { dispatchIfPresent } from '../chunk-
|
|
2
|
-
import { StoreConflictError } from '../chunk-
|
|
3
|
-
import {
|
|
1
|
+
import { dispatchIfPresent } from '../chunk-BVFS3ICC.js';
|
|
2
|
+
import { StoreConflictError } from '../chunk-A3QPMX5M.js';
|
|
3
|
+
import { mkdir, writeFile, rename, readFile, unlink, open, stat } from 'fs/promises';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
5
|
import { dirname, join } from 'path';
|
|
6
6
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
7
|
+
import { uptime, hostname } from 'os';
|
|
7
8
|
|
|
8
9
|
var AsyncLock = class {
|
|
9
10
|
tail = Promise.resolve();
|
|
@@ -60,6 +61,172 @@ function makeJournal(backend) {
|
|
|
60
61
|
}
|
|
61
62
|
};
|
|
62
63
|
}
|
|
64
|
+
var DEFAULT_LIMITS = { waitMs: 1e4, graceMs: 2e3 };
|
|
65
|
+
var SPIN_MS = 10;
|
|
66
|
+
var BOOT_TOLERANCE_MS = 6e4;
|
|
67
|
+
var BLOCK_EXPLAINED = {
|
|
68
|
+
"another-host": "it was written on another host, which may still be writing, so it is not stolen. If that host is gone, delete the file and try again.",
|
|
69
|
+
"live-holder": "the process it names is alive. If that process is not a writer here, delete the file and try again.",
|
|
70
|
+
unwritten: "it is empty and too young to count as a crash.",
|
|
71
|
+
unreadable: "its contents are not a lock this version can read. If no writer holds it, delete the file and try again.",
|
|
72
|
+
contended: "other writers kept taking it."
|
|
73
|
+
};
|
|
74
|
+
var FileLockTimeoutError = class extends Error {
|
|
75
|
+
constructor(lockPath, waitedMs, blockedBy) {
|
|
76
|
+
super(
|
|
77
|
+
`@demlik/tea: gave up on lock "${lockPath}" after ${Math.round(waitedMs)} ms: ${BLOCK_EXPLAINED[blockedBy]}`
|
|
78
|
+
);
|
|
79
|
+
this.lockPath = lockPath;
|
|
80
|
+
this.waitedMs = waitedMs;
|
|
81
|
+
this.blockedBy = blockedBy;
|
|
82
|
+
}
|
|
83
|
+
lockPath;
|
|
84
|
+
waitedMs;
|
|
85
|
+
blockedBy;
|
|
86
|
+
name = "FileLockTimeoutError";
|
|
87
|
+
_tag = "file_lock_timeout";
|
|
88
|
+
};
|
|
89
|
+
function thisMachine() {
|
|
90
|
+
return {
|
|
91
|
+
host: hostname(),
|
|
92
|
+
bootedAtMs: Math.round(Date.now() - uptime() * 1e3)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function isValidPid(pid) {
|
|
96
|
+
return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0;
|
|
97
|
+
}
|
|
98
|
+
function parseStamp(body) {
|
|
99
|
+
let raw;
|
|
100
|
+
try {
|
|
101
|
+
raw = JSON.parse(body);
|
|
102
|
+
} catch {
|
|
103
|
+
return { kind: "unreadable" };
|
|
104
|
+
}
|
|
105
|
+
if (typeof raw !== "object" || raw === null) return { kind: "unreadable" };
|
|
106
|
+
const { host, bootedAtMs, pid } = raw;
|
|
107
|
+
if (typeof host !== "string" || typeof bootedAtMs !== "number" || !Number.isFinite(bootedAtMs) || !isValidPid(pid)) {
|
|
108
|
+
return { kind: "unreadable" };
|
|
109
|
+
}
|
|
110
|
+
return { kind: "stamped", host, bootedAtMs, pid };
|
|
111
|
+
}
|
|
112
|
+
function parseHolder(text) {
|
|
113
|
+
const body = text.trim();
|
|
114
|
+
if (body === "") return { kind: "unwritten" };
|
|
115
|
+
if (/^\d+$/.test(body)) {
|
|
116
|
+
const pid = Number(body);
|
|
117
|
+
return isValidPid(pid) ? { kind: "pid-only", pid } : { kind: "unreadable" };
|
|
118
|
+
}
|
|
119
|
+
return parseStamp(body);
|
|
120
|
+
}
|
|
121
|
+
function isProcessAlive(pid) {
|
|
122
|
+
try {
|
|
123
|
+
process.kill(pid, 0);
|
|
124
|
+
return true;
|
|
125
|
+
} catch (err) {
|
|
126
|
+
return err.code === "EPERM";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function judgeHolder(holder, ageMs, here, graceMs) {
|
|
130
|
+
switch (holder.kind) {
|
|
131
|
+
case "unwritten":
|
|
132
|
+
return ageMs > graceMs ? "stale" : "unwritten";
|
|
133
|
+
case "unreadable":
|
|
134
|
+
return "unreadable";
|
|
135
|
+
case "pid-only":
|
|
136
|
+
return isProcessAlive(holder.pid) ? "live-holder" : "stale";
|
|
137
|
+
case "stamped":
|
|
138
|
+
if (holder.host !== here.host) return "another-host";
|
|
139
|
+
if (Math.abs(holder.bootedAtMs - here.bootedAtMs) > BOOT_TOLERANCE_MS) {
|
|
140
|
+
return "stale";
|
|
141
|
+
}
|
|
142
|
+
return isProcessAlive(holder.pid) ? "live-holder" : "stale";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function ageOf(path) {
|
|
146
|
+
try {
|
|
147
|
+
return Date.now() - (await stat(path)).mtimeMs;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (err.code === "ENOENT") return "vanished";
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function inspectLock(lockPath, limits) {
|
|
154
|
+
let text;
|
|
155
|
+
try {
|
|
156
|
+
text = await readFile(lockPath, "utf8");
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (err.code === "ENOENT") return "vanished";
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
const ageMs = await ageOf(lockPath);
|
|
162
|
+
if (ageMs === "vanished") return "vanished";
|
|
163
|
+
return judgeHolder(parseHolder(text), ageMs, thisMachine(), limits.graceMs);
|
|
164
|
+
}
|
|
165
|
+
async function createExclusive(path, body) {
|
|
166
|
+
try {
|
|
167
|
+
const handle = await open(path, "wx");
|
|
168
|
+
try {
|
|
169
|
+
if (body !== "") await handle.writeFile(body);
|
|
170
|
+
} finally {
|
|
171
|
+
await handle.close();
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (err.code === "EEXIST") return false;
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
var removeIfPresent = (path) => unlink(path).catch((err) => {
|
|
180
|
+
if (err.code !== "ENOENT") throw err;
|
|
181
|
+
});
|
|
182
|
+
async function stealIfStillStale(lockPath, limits) {
|
|
183
|
+
const guardPath = `${lockPath}.steal`;
|
|
184
|
+
if (!await createExclusive(guardPath, "")) {
|
|
185
|
+
const age = await ageOf(guardPath);
|
|
186
|
+
if (age !== "vanished" && age > limits.graceMs) {
|
|
187
|
+
await removeIfPresent(guardPath);
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const verdict = await inspectLock(lockPath, limits);
|
|
193
|
+
if (verdict === "stale") await removeIfPresent(lockPath);
|
|
194
|
+
return verdict === "stale" || verdict === "vanished";
|
|
195
|
+
} finally {
|
|
196
|
+
await removeIfPresent(guardPath);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function acquireFileLock(dir, lockPath, limits) {
|
|
200
|
+
await mkdir(dir, { recursive: true });
|
|
201
|
+
const startedAt = performance.now();
|
|
202
|
+
let blockedBy = "contended";
|
|
203
|
+
for (; ; ) {
|
|
204
|
+
const here = thisMachine();
|
|
205
|
+
const stamp = JSON.stringify({
|
|
206
|
+
host: here.host,
|
|
207
|
+
bootedAtMs: here.bootedAtMs,
|
|
208
|
+
pid: process.pid
|
|
209
|
+
});
|
|
210
|
+
if (await createExclusive(lockPath, stamp)) return;
|
|
211
|
+
const verdict = await inspectLock(lockPath, limits);
|
|
212
|
+
if (verdict !== "stale" && verdict !== "vanished") blockedBy = verdict;
|
|
213
|
+
const freed = verdict === "vanished" || verdict === "stale" && await stealIfStillStale(lockPath, limits);
|
|
214
|
+
const waitedMs = performance.now() - startedAt;
|
|
215
|
+
if (waitedMs >= limits.waitMs) {
|
|
216
|
+
throw new FileLockTimeoutError(lockPath, waitedMs, blockedBy);
|
|
217
|
+
}
|
|
218
|
+
if (!freed) await new Promise((resolve) => setTimeout(resolve, SPIN_MS));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function withFileLock(dir, lockPath, fx, limits = DEFAULT_LIMITS) {
|
|
222
|
+
await acquireFileLock(dir, lockPath, limits);
|
|
223
|
+
try {
|
|
224
|
+
return await fx();
|
|
225
|
+
} finally {
|
|
226
|
+
await unlink(lockPath).catch(() => {
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
63
230
|
|
|
64
231
|
// src/node/index.ts
|
|
65
232
|
function fileStore(path, parse, options = {}) {
|
|
@@ -123,9 +290,7 @@ function fenceFileStore(base, path) {
|
|
|
123
290
|
return { raw: await base.load(), version };
|
|
124
291
|
},
|
|
125
292
|
async saveFenced(state, expectedVersion) {
|
|
126
|
-
|
|
127
|
-
await acquireFileLock(dirname(path), lockPath);
|
|
128
|
-
try {
|
|
293
|
+
return withFileLock(dirname(path), lockPath, async () => {
|
|
129
294
|
const actual = await readVersion();
|
|
130
295
|
if (actual !== expectedVersion) {
|
|
131
296
|
throw new StoreConflictError(expectedVersion, actual);
|
|
@@ -136,23 +301,13 @@ function fenceFileStore(base, path) {
|
|
|
136
301
|
await writeFile(tmp, String(next), "utf8");
|
|
137
302
|
await rename(tmp, fencePath);
|
|
138
303
|
return next;
|
|
139
|
-
}
|
|
140
|
-
await unlink(lockPath).catch(() => {
|
|
141
|
-
});
|
|
142
|
-
}
|
|
304
|
+
});
|
|
143
305
|
},
|
|
144
306
|
async delete() {
|
|
145
|
-
await
|
|
146
|
-
try {
|
|
147
|
-
await base.delete();
|
|
148
|
-
} finally {
|
|
149
|
-
await unlink(lockPath).catch(() => {
|
|
150
|
-
});
|
|
151
|
-
}
|
|
307
|
+
await withFileLock(dirname(path), lockPath, () => base.delete());
|
|
152
308
|
}
|
|
153
309
|
};
|
|
154
310
|
}
|
|
155
|
-
var LOCK_SPIN_MS = 10;
|
|
156
311
|
function fileJournal(dir, parse) {
|
|
157
312
|
const streamPath = (stream) => join(dir, `${encodeURIComponent(stream)}.jsonl`);
|
|
158
313
|
const readEntries = async (stream) => {
|
|
@@ -188,58 +343,10 @@ function fileJournal(dir, parse) {
|
|
|
188
343
|
return entry;
|
|
189
344
|
},
|
|
190
345
|
async lock(stream, fx) {
|
|
191
|
-
|
|
192
|
-
await acquireFileLock(dir, lockPath);
|
|
193
|
-
try {
|
|
194
|
-
return await fx();
|
|
195
|
-
} finally {
|
|
196
|
-
await unlink(lockPath).catch(() => {
|
|
197
|
-
});
|
|
198
|
-
}
|
|
346
|
+
return withFileLock(dir, `${streamPath(stream)}.lock`, fx);
|
|
199
347
|
}
|
|
200
348
|
});
|
|
201
349
|
}
|
|
202
|
-
async function acquireFileLock(dir, lockPath) {
|
|
203
|
-
await mkdir(dir, { recursive: true });
|
|
204
|
-
for (; ; ) {
|
|
205
|
-
try {
|
|
206
|
-
const handle = await open(lockPath, "wx");
|
|
207
|
-
try {
|
|
208
|
-
await handle.writeFile(String(process.pid));
|
|
209
|
-
} finally {
|
|
210
|
-
await handle.close();
|
|
211
|
-
}
|
|
212
|
-
return;
|
|
213
|
-
} catch (err) {
|
|
214
|
-
if (err.code !== "EEXIST") throw err;
|
|
215
|
-
if (await stealIfStale(lockPath)) continue;
|
|
216
|
-
await new Promise((resolve) => setTimeout(resolve, LOCK_SPIN_MS));
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
async function stealIfStale(lockPath) {
|
|
221
|
-
let holder;
|
|
222
|
-
try {
|
|
223
|
-
holder = await readFile(lockPath, "utf8");
|
|
224
|
-
} catch (err) {
|
|
225
|
-
if (err.code === "ENOENT") return true;
|
|
226
|
-
throw err;
|
|
227
|
-
}
|
|
228
|
-
const pid = Number(holder.trim());
|
|
229
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
230
|
-
if (isProcessAlive(pid)) return false;
|
|
231
|
-
await unlink(lockPath).catch(() => {
|
|
232
|
-
});
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
function isProcessAlive(pid) {
|
|
236
|
-
try {
|
|
237
|
-
process.kill(pid, 0);
|
|
238
|
-
return true;
|
|
239
|
-
} catch (err) {
|
|
240
|
-
return err.code === "EPERM";
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
350
|
var cachedWs;
|
|
244
351
|
function loadWebSocket() {
|
|
245
352
|
if (cachedWs) return cachedWs;
|
|
@@ -328,6 +435,6 @@ function nodeWsRunner(handlers) {
|
|
|
328
435
|
};
|
|
329
436
|
}
|
|
330
437
|
|
|
331
|
-
export { fileJournal, fileStore, nodeSubscribe, sendToWebSocket };
|
|
438
|
+
export { FileLockTimeoutError, fileJournal, fileStore, nodeSubscribe, sendToWebSocket };
|
|
332
439
|
//# sourceMappingURL=index.js.map
|
|
333
440
|
//# sourceMappingURL=index.js.map
|