@executablemd/durable-streams 0.6.0 → 0.7.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/esm/guard.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * guardDurableStream — a host-side pre-persistence boundary.
3
+ *
4
+ * A gate runs once per live append, before the event reaches the backend.
5
+ * It receives a copy and returns nothing, so it can inspect or reject but
6
+ * never rewrite. When the gate completes, the original event is handed to
7
+ * the underlying stream exactly once. When the gate fails or is cancelled,
8
+ * the backend is never invoked and the failure propagates to the durable
9
+ * effect that produced the event.
10
+ *
11
+ * The backend append is a statement after the gate rather than a
12
+ * continuation passed to it. A gate therefore has nothing it can invoke
13
+ * twice, which preserves the protocol invariant that one durable yield
14
+ * produces at most one journal event.
15
+ */
16
+ /**
17
+ * Wrap a durable stream so every live append passes through `gate` first.
18
+ *
19
+ * `readAll()` delegates straight to the underlying stream, so replaying a
20
+ * journal restores existing entries without gating them.
21
+ *
22
+ * Wrap the stream before execution begins to cover the complete live
23
+ * journal — root component imports, yields, child closes, and the root
24
+ * close.
25
+ *
26
+ * Rejection is per event. The rejected event never reaches the backend, but
27
+ * the resulting failure may lead the workflow to append a later `Close`
28
+ * event with an `err` result, and that close crosses the gate on its own.
29
+ */
30
+ export function guardDurableStream(stream, gate) {
31
+ return {
32
+ readAll: () => stream.readAll(),
33
+ *append(event) {
34
+ // The gate sees a copy so "inspect or reject" is enforced rather than
35
+ // merely documented: the backend always receives the event the effect
36
+ // produced, whatever the gate did to the one it was handed.
37
+ yield* gate(structuredClone(event));
38
+ yield* stream.append(event);
39
+ },
40
+ };
41
+ }
package/esm/mod.js CHANGED
@@ -8,6 +8,8 @@
8
8
  // ReplayIndex
9
9
  export { ReplayIndex } from "./replay-index.js";
10
10
  export { InMemoryStream } from "./stream.js";
11
+ // Pre-persistence gate — runs before an event reaches its backend
12
+ export { guardDurableStream } from "./guard.js";
11
13
  // HTTP-backed stream adapter
12
14
  export { useHttpDurableStream } from "./http-stream.js";
13
15
  // Errors
@@ -19,7 +21,7 @@ export { ReplayGuard } from "./replay-guard.js";
19
21
  // Context
20
22
  export { DurableCtx } from "./context.js";
21
23
  // Serialization utilities
22
- export { deserializeError, effectionToProtocol, protocolToEffection, serializeError, } from "./serialize.js";
24
+ export { deserializeError, effectionToProtocol, protocolToEffection, serializeDurableEvent, serializeError, } from "./serialize.js";
23
25
  // Core effect factories
24
26
  export { createDurableEffect, createDurableOperation } from "./effect.js";
25
27
  // Workflow-enabled effects
package/esm/serialize.js CHANGED
@@ -5,7 +5,36 @@
5
5
  * - Protocol Result ({ status: "ok" | "err" | "cancelled" })
6
6
  * - Effection Result ({ ok: true, value } | { ok: false, error })
7
7
  * - Error ↔ SerializedError
8
+ *
9
+ * `serializeDurableEvent` is the shared NDJSON representation: file
10
+ * persistence writes it, and gates that inspect the persisted form derive
11
+ * it from the same function.
8
12
  */
13
+ /**
14
+ * Render one durable event as its NDJSON record, terminating newline
15
+ * included.
16
+ *
17
+ * This is ordinary `JSON.stringify(event) + "\n"`. Field order follows the
18
+ * event object's own insertion order; nothing is sorted, normalized, or
19
+ * otherwise canonicalized. The value of sharing it is that file persistence
20
+ * and anything inspecting the persisted form cannot drift apart, not that
21
+ * the output is a canonical form of the event.
22
+ *
23
+ * Serialization fails when `JSON.stringify` throws — a circular structure or
24
+ * a `BigInt` — or when it does not return a string. Values that
25
+ * `JSON.stringify` silently coerces or drops are left alone: `undefined`,
26
+ * function and symbol members are omitted, non-finite numbers become `null`,
27
+ * and non-plain objects serialize through `toJSON`.
28
+ */
29
+ export function serializeDurableEvent(event) {
30
+ const record = JSON.stringify(event);
31
+ // JSON.stringify returns undefined rather than throwing when the value
32
+ // itself has no JSON representation.
33
+ if (typeof record !== "string") {
34
+ throw new TypeError("serializeDurableEvent: event has no JSON representation");
35
+ }
36
+ return `${record}\n`;
37
+ }
9
38
  /** Serialize an Error to a JSON-safe SerializedError. */
10
39
  export function serializeError(error) {
11
40
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@executablemd/durable-streams",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Durable, replayable event streams for executable.md.",
5
5
  "homepage": "https://executable.md",
6
6
  "repository": {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * guardDurableStream — a host-side pre-persistence boundary.
3
+ *
4
+ * A gate runs once per live append, before the event reaches the backend.
5
+ * It receives a copy and returns nothing, so it can inspect or reject but
6
+ * never rewrite. When the gate completes, the original event is handed to
7
+ * the underlying stream exactly once. When the gate fails or is cancelled,
8
+ * the backend is never invoked and the failure propagates to the durable
9
+ * effect that produced the event.
10
+ *
11
+ * The backend append is a statement after the gate rather than a
12
+ * continuation passed to it. A gate therefore has nothing it can invoke
13
+ * twice, which preserves the protocol invariant that one durable yield
14
+ * produces at most one journal event.
15
+ */
16
+ import type { Operation } from "effection";
17
+ import type { DurableStream } from "./stream.js";
18
+ import type { DurableEvent } from "./types.js";
19
+ /**
20
+ * A check that runs before a durable event is persisted.
21
+ *
22
+ * Completing successfully admits the event. Failing or being cancelled
23
+ * rejects it. A gate inspects or rejects, and nothing else: it returns no
24
+ * value, and the event it receives is a copy, so writing to that copy
25
+ * changes nothing about what gets journaled.
26
+ */
27
+ export type DurableEventGate = (event: DurableEvent) => Operation<void>;
28
+ /**
29
+ * Wrap a durable stream so every live append passes through `gate` first.
30
+ *
31
+ * `readAll()` delegates straight to the underlying stream, so replaying a
32
+ * journal restores existing entries without gating them.
33
+ *
34
+ * Wrap the stream before execution begins to cover the complete live
35
+ * journal — root component imports, yields, child closes, and the root
36
+ * close.
37
+ *
38
+ * Rejection is per event. The rejected event never reaches the backend, but
39
+ * the resulting failure may lead the workflow to append a later `Close`
40
+ * event with an `err` result, and that close crosses the gate on its own.
41
+ */
42
+ export declare function guardDurableStream(stream: DurableStream, gate: DurableEventGate): DurableStream;
package/types/mod.d.ts CHANGED
@@ -10,6 +10,8 @@ export { ReplayIndex } from "./replay-index.js";
10
10
  export type { YieldEntry } from "./replay-index.js";
11
11
  export type { DurableStream } from "./stream.js";
12
12
  export { InMemoryStream } from "./stream.js";
13
+ export { guardDurableStream } from "./guard.js";
14
+ export type { DurableEventGate } from "./guard.js";
13
15
  export { useHttpDurableStream } from "./http-stream.js";
14
16
  export type { HttpDurableStreamHandle, HttpDurableStreamOptions } from "./http-stream.js";
15
17
  export { ContinuePastCloseDivergenceError, DivergenceError, EarlyReturnDivergenceError, StaleInputError, } from "./errors.js";
@@ -19,7 +21,7 @@ export { ReplayGuard } from "./replay-guard.js";
19
21
  export type { ReplayOutcome } from "./replay-guard.js";
20
22
  export { DurableCtx } from "./context.js";
21
23
  export type { DurableContext } from "./context.js";
22
- export { deserializeError, effectionToProtocol, protocolToEffection, serializeError, } from "./serialize.js";
24
+ export { deserializeError, effectionToProtocol, protocolToEffection, serializeDurableEvent, serializeError, } from "./serialize.js";
23
25
  export { createDurableEffect, createDurableOperation } from "./effect.js";
24
26
  export type { Executor } from "./effect.js";
25
27
  export { durableAction, durableCall, durableSleep, versionCheck } from "./operations.js";
@@ -5,8 +5,29 @@
5
5
  * - Protocol Result ({ status: "ok" | "err" | "cancelled" })
6
6
  * - Effection Result ({ ok: true, value } | { ok: false, error })
7
7
  * - Error ↔ SerializedError
8
+ *
9
+ * `serializeDurableEvent` is the shared NDJSON representation: file
10
+ * persistence writes it, and gates that inspect the persisted form derive
11
+ * it from the same function.
12
+ */
13
+ import type { DurableEvent, EffectionResult, Result, SerializedError } from "./types.js";
14
+ /**
15
+ * Render one durable event as its NDJSON record, terminating newline
16
+ * included.
17
+ *
18
+ * This is ordinary `JSON.stringify(event) + "\n"`. Field order follows the
19
+ * event object's own insertion order; nothing is sorted, normalized, or
20
+ * otherwise canonicalized. The value of sharing it is that file persistence
21
+ * and anything inspecting the persisted form cannot drift apart, not that
22
+ * the output is a canonical form of the event.
23
+ *
24
+ * Serialization fails when `JSON.stringify` throws — a circular structure or
25
+ * a `BigInt` — or when it does not return a string. Values that
26
+ * `JSON.stringify` silently coerces or drops are left alone: `undefined`,
27
+ * function and symbol members are omitted, non-finite numbers become `null`,
28
+ * and non-plain objects serialize through `toJSON`.
8
29
  */
9
- import type { EffectionResult, Result, SerializedError } from "./types.js";
30
+ export declare function serializeDurableEvent(event: DurableEvent): string;
10
31
  /** Serialize an Error to a JSON-safe SerializedError. */
11
32
  export declare function serializeError(error: Error): SerializedError;
12
33
  /** Deserialize a SerializedError back to an Error. */