@executablemd/durable-streams 0.5.2 → 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 +41 -0
- package/esm/mod.js +3 -1
- package/esm/serialize.js +29 -0
- package/package.json +2 -2
- package/types/guard.d.ts +42 -0
- package/types/mod.d.ts +3 -1
- package/types/serialize.d.ts +22 -1
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.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Durable, replayable event streams for executable.md.",
|
|
5
5
|
"homepage": "https://executable.md",
|
|
6
6
|
"repository": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"scripts": {},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@durable-streams/client": "^0.2.2",
|
|
27
|
-
"effection": "4.1.0-alpha.
|
|
27
|
+
"effection": "4.1.0-alpha.10"
|
|
28
28
|
},
|
|
29
29
|
"_generatedBy": "dnt@dev"
|
|
30
30
|
}
|
package/types/guard.d.ts
ADDED
|
@@ -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";
|
package/types/serialize.d.ts
CHANGED
|
@@ -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
|
-
|
|
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. */
|