@effect-uai/ui 0.19.0-dev-20261110065930 → 0.20.0-dev-20261110202152
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/dist/Atoms.d.mts +13 -4
- package/dist/Atoms.d.mts.map +1 -1
- package/dist/Atoms.mjs +8 -2
- package/dist/Atoms.mjs.map +1 -1
- package/dist/{Client-C9UYN_VK.d.mts → Client-B2Eqo39q.d.mts} +5 -4
- package/dist/{Client-C9UYN_VK.d.mts.map → Client-B2Eqo39q.d.mts.map} +1 -1
- package/dist/Client.d.mts +1 -1
- package/dist/Client.mjs +8 -5
- package/dist/Client.mjs.map +1 -1
- package/dist/Run.d.mts +2 -2
- package/dist/Run.d.mts.map +1 -1
- package/dist/SseEncoding.d.mts +10 -3
- package/dist/SseEncoding.d.mts.map +1 -1
- package/dist/SseEncoding.mjs +10 -1
- package/dist/SseEncoding.mjs.map +1 -1
- package/dist/Transport.d.mts +58 -17
- package/dist/Transport.d.mts.map +1 -1
- package/dist/Transport.mjs +81 -21
- package/dist/Transport.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/package.json +4 -4
- package/src/Atoms.test.ts +30 -0
- package/src/Atoms.ts +16 -7
- package/src/Client.ts +9 -7
- package/src/SseEncoding.test.ts +20 -1
- package/src/SseEncoding.ts +18 -1
- package/src/Transport.test.ts +199 -0
- package/src/Transport.ts +173 -34
package/dist/Atoms.d.mts
CHANGED
|
@@ -7,7 +7,7 @@ import { Effect, Schema } from "effect";
|
|
|
7
7
|
import { Atom } from "effect/reactivity";
|
|
8
8
|
import { SecureRandom } from "effect/crypto";
|
|
9
9
|
declare namespace Atoms_d_exports {
|
|
10
|
-
export { Busy, Decode, Options, Runtime, SessionOptions, make, session, sessionThread, thread };
|
|
10
|
+
export { Busy, Decode, Open, Options, Runtime, SessionOptions, make, session, sessionThread, thread };
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* What `submit` does while a run is in flight. `"block"` ignores the new
|
|
@@ -103,19 +103,26 @@ type BaseSessionOptions<I, S, E, R, ER, D> = {
|
|
|
103
103
|
readonly ended?: (state: S, reason: "stopped" | "disconnected") => S;
|
|
104
104
|
};
|
|
105
105
|
type SessionOptions<I, S, E, R, ER, D = Envelope> = BaseSessionOptions<I, S, E, R, ER, D> & WithDecode<D>;
|
|
106
|
+
/** What `open` follows: a session from its start, or from `after` (e.g. a hydrated thread's `lastSeq`). */
|
|
107
|
+
type Open = StreamId | {
|
|
108
|
+
readonly streamId: StreamId;
|
|
109
|
+
readonly after: number;
|
|
110
|
+
};
|
|
106
111
|
declare const buildSession: <I, S, E, R, ER, D>(options: BaseSessionOptions<I, S, E, R, ER, D> & {
|
|
107
112
|
readonly decode: Decode<D>;
|
|
108
113
|
}) => {
|
|
109
114
|
state: Atom.Writable<S, S>;
|
|
110
115
|
streamId: Atom.Writable<(string & import("effect/Brand").Brand<"StreamId">) | null, (string & import("effect/Brand").Brand<"StreamId">) | null>;
|
|
111
|
-
open: Atom.AtomResultFn<
|
|
116
|
+
open: Atom.AtomResultFn<Open, unknown, unknown>;
|
|
112
117
|
submit: Atom.AtomResultFn<Draft<I>, unknown, unknown>;
|
|
113
118
|
stop: Atom.AtomResultFn<void, void, E | ER>;
|
|
114
119
|
};
|
|
115
120
|
type BuiltSession<I, S, E, R, ER> = ReturnType<typeof buildSession<I, S, E, R, ER, unknown>>;
|
|
116
121
|
/**
|
|
117
122
|
* State over a session transport. `open(streamId)` follows a session's
|
|
118
|
-
* stream from its start (e.g. a chat id kept across reloads)
|
|
123
|
+
* stream from its start (e.g. a chat id kept across reloads);
|
|
124
|
+
* `open({ streamId, after })` from a position, for state hydrated elsewhere
|
|
125
|
+
* (pass it as `initial`; `initial` never opens anything). `submit` sends
|
|
119
126
|
* an input; without an open session the server starts one, which is then
|
|
120
127
|
* opened. Runs arrive on the followed stream, not as responses to `submit`.
|
|
121
128
|
*/
|
|
@@ -124,6 +131,8 @@ type SessionThreadOptions<I, E, R, ER, P extends Payloads> = {
|
|
|
124
131
|
readonly runtime: Runtime<R, ER>;
|
|
125
132
|
readonly transport: Session<Sent<I>, Thread<P>, E, R>;
|
|
126
133
|
readonly isBusy?: (thread: Thread<P>) => boolean;
|
|
134
|
+
/** A thread hydrated elsewhere (e.g. from a database); follow it with `open({ streamId, after: lastSeq })`. */
|
|
135
|
+
readonly initial?: Thread<P>;
|
|
127
136
|
};
|
|
128
137
|
/**
|
|
129
138
|
* `session` with the `Thread` preset. Input sent while a run is active, or
|
|
@@ -137,5 +146,5 @@ declare function sessionThread<I extends Input, E, R, ER, P extends Payloads>(op
|
|
|
137
146
|
}): BuiltSession<I, Thread<P>, E, R, ER>;
|
|
138
147
|
declare function sessionThread<I extends Input, E, R, ER>(options: SessionThreadOptions<I, E, R, ER, Untyped>): BuiltSession<I, Thread, E, R, ER>;
|
|
139
148
|
//#endregion
|
|
140
|
-
export { Busy, Decode, Options, Runtime, SessionOptions, make, session, sessionThread, Atoms_d_exports as t, thread };
|
|
149
|
+
export { Busy, Decode, Open, Options, Runtime, SessionOptions, make, session, sessionThread, Atoms_d_exports as t, thread };
|
|
141
150
|
//# sourceMappingURL=Atoms.d.mts.map
|
package/dist/Atoms.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Atoms.d.mts","names":[],"sources":["../src/Atoms.ts"],"mappings":";;;;;;;;;;;;;;;;KAyBY;;;;;;KAOA,QAAQ,GAAG,MAAM,KAAK,YAAY,IAAI,aAAa,cAAc;;KAGjE,OAAO,MAAM,UAAU,aAAa,OAAO,OAAO,GAAG,OAAO;KAGnE,WAAW,MAAM,mBAAmB;WAC1B,SAAS,OAAO;;WAChB,QAAQ,OAAO;;;;;;;;KAQzB,YAAY,GAAG,GAAG,GAAG,GAAG,IAAI;WACtB,SAAS,QAAQ,GAAG;WACpB,WAAW,UAAoB,KAAW,IAAI,GAAG,GAAG;WACpD,SAAS;WACT,OAAO,OAAO,GAAG,UAAU,MAAM;WACjC,MAAM;WACN,UAAU,OAAO;;WAEjB,aAAa,OAAO,GAAG,OAAO,KAAW,OAAO;;WAEhD,SAAS,OAAO,GAAG,uCAAuC;;KAGzD,QAAQ,GAAG,GAAG,GAAG,GAAG,IAAI,IAAI,YAAY,YAAY,GAAG,GAAG,GAAG,GAAG,IAAI,KAAK,WAAW;cAI1F,QAAS,GAAG,GAAG,GAAG,GAAG,IAAI,GAAC,SACrB,YAAY,GAAG,GAAG,GAAG,GAAG,IAAI;WAAgB,QAAQ,OAAO;;;;;;;;;;aAwEnC,UAAA;aAA0B;;;KAiBxD,MAAM,GAAG,GAAG,GAAG,GAAG,MAAM,kBAAkB,MAAM,GAAG,GAAG,GAAG,GAAG;;;;;;iBAOjD,KAAK,GAAG,GAAG,GAAG,GAAG,IAAI,IAAI,UACvC,SAAS,QAAQ,GAAG,GAAG,GAAG,GAAG,IAAI;WACtB,WAAW,QAAkB,KAAW,IAAI,GAAG,GAAG;IAE5D,KAAK,MAAM,GAAG,GAAG,GAAG,GAAG;WACf,QAAQ,YAAY,MAAM,GAAG,GAAG,GAAG,GAAG;;iBAEjC,KAAK,GAAG,GAAG,GAAG,GAAG,IAAI,IAAI,UACvC,SAAS,QAAQ,GAAG,GAAG,GAAG,GAAG,IAAI,KAChC,MAAM,GAAG,GAAG,GAAG,GAAG;KAOhB,cAAc,GAAG,GAAG,GAAG,IAAI,UAAU;WAC/B,SAAS,QAAQ,GAAG;WACpB,WAAW,UAAoB,KAAW,IAAI,OAAc,IAAI,GAAG;WACnE,MAAM;WACN,UAAU,OAAc;;;;;;;;iBASnB,OAAO,UAAU,OAAa,GAAG,GAAG,IAAI,UAAU,UAChE,SAAS,cAAc,GAAG,GAAG,GAAG,IAAI;WAAgB,UAAU,WAAW;IACxE,MAAM,GAAG,OAAc,IAAI,GAAG,GAAG;iBACpB,OAAO,UAAU,OAAa,GAAG,GAAG,IAClD,SAAS,cAAc,GAAG,GAAG,GAAG,IAAI,WACnC,MAAM,GAAG,QAAe,GAAG,GAAG;KAwB5B,mBAAmB,GAAG,GAAG,GAAG,GAAG,IAAI;WAC7B,SAAS,QAAQ,GAAG;WACpB,WAAW,QAAkB,KAAW,IAAI,GAAG,GAAG;WAClD,SAAS;WACT,OAAO,OAAO,GAAG,UAAU,MAAM;;WAEjC,UAAU,OAAO;;WAEjB,aAAa,OAAO,GAAG,OAAO,KAAW,OAAO;;WAEhD,SAAS,OAAO,GAAG,uCAAuC;;KAGzD,eAAe,GAAG,GAAG,GAAG,GAAG,IAAI,IAAI,YAAY,mBAAmB,GAAG,GAAG,GAAG,GAAG,IAAI,KAC5F,WAAW;
|
|
1
|
+
{"version":3,"file":"Atoms.d.mts","names":[],"sources":["../src/Atoms.ts"],"mappings":"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"}
|
package/dist/Atoms.mjs
CHANGED
|
@@ -86,7 +86,13 @@ const buildSession = (options) => {
|
|
|
86
86
|
const ended = options.ended;
|
|
87
87
|
return ended === void 0 || Exit.isSuccess(exit) || Exit.hasInterrupts(exit) ? Effect.void : Atom.update(state, (s) => ended(s, "disconnected"));
|
|
88
88
|
};
|
|
89
|
-
const open = options.runtime.fn()((
|
|
89
|
+
const open = options.runtime.fn()((target) => {
|
|
90
|
+
const { streamId: id, after } = typeof target === "string" ? {
|
|
91
|
+
streamId: target,
|
|
92
|
+
after: 0
|
|
93
|
+
} : target;
|
|
94
|
+
return Atom.set(streamId, id).pipe(Effect.andThen(options.transport.events(id, after).pipe(Stream.runForEach((envelope) => options.decode(envelope).pipe(Effect.flatMap((decoded) => Atom.update(state, (s) => options.step(s, decoded))))))), Effect.onExit(settle));
|
|
95
|
+
}).pipe(Atom.keepAlive);
|
|
90
96
|
return {
|
|
91
97
|
state,
|
|
92
98
|
streamId,
|
|
@@ -118,7 +124,7 @@ function sessionThread(options) {
|
|
|
118
124
|
return buildSession({
|
|
119
125
|
runtime: options.runtime,
|
|
120
126
|
transport: options.transport,
|
|
121
|
-
initial: fold.initial,
|
|
127
|
+
initial: options.initial ?? fold.initial,
|
|
122
128
|
step: fold.step,
|
|
123
129
|
decode: options.contract?.decode ?? identity,
|
|
124
130
|
...options.isBusy !== void 0 ? { isBusy: options.isBusy } : {},
|
package/dist/Atoms.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Atoms.mjs","names":["Input.withId","Thread.presetFor"],"sources":["../src/Atoms.ts"],"sourcesContent":["/**\n * Framework-neutral state over a transport, built on `effect/reactivity`.\n * Per-run transports: `make` takes any fold (`initial` + `step`), `thread`\n * is the `Thread` preset. Session transports: `session` and\n * `sessionThread`, likewise. React reads them with `@effect/atom-react` (`useAtomValue`,\n * `useAtomSet`), Vue and Solid with their official bindings, plain JS with\n * `AtomRegistry.subscribe`.\n *\n * With a `UiContract`, every envelope is decoded once on arrival, so the\n * state holds decoded payloads and is typed by the contract.\n */\nimport { Effect, Exit, Option, type Schema, Stream } from \"effect\"\nimport type { SecureRandom } from \"effect/crypto\"\nimport { Atom } from \"effect/reactivity\"\nimport * as Input from \"./Input.js\"\nimport type { Envelope, Payloads, StreamId, Untyped } from \"./RunEvent.js\"\nimport * as Thread from \"./Thread.js\"\nimport type * as Transport from \"./Transport.js\"\nimport type { UiContract } from \"./UiContract.js\"\n\n/**\n * What `submit` does while a run is in flight. `\"block\"` ignores the new\n * input; `\"stop-then-submit\"` abandons the current run locally and starts\n * the new one (in durable mode the server-side run keeps going).\n */\nexport type Busy = \"block\" | \"stop-then-submit\"\n\n/**\n * The atoms' runtime: the transport's services plus `SecureRandom` for\n * input ids, e.g. `Atom.runtime(Layer.mergeAll(FetchHttpClient.layer,\n * SecureRandom.layerWebCrypto))`. A test provides a seeded one.\n */\nexport type Runtime<R, ER> = Atom.AtomRuntime<R | SecureRandom.SecureRandom, ER>\n\n/** Turns a wire envelope into what `step` folds (a contract's `decode`). */\nexport type Decode<D> = (envelope: Envelope) => Effect.Effect<D, Schema.SchemaError>\n\n// `decode` may be left out only when `step` folds wire envelopes.\ntype WithDecode<D> = [Envelope] extends [D]\n ? { readonly decode?: Decode<D> }\n : { readonly decode: Decode<D> }\n\n/**\n * `I` is the app's input type. `submit` takes a draft of it (`id` optional);\n * the transport receives it with the id assigned (`Input.Sent<I>`, a UUIDv7\n * from `SecureRandom`). `D` is the envelope `step` folds: the wire envelope,\n * or a contract's decoded one (then pass `decode`).\n */\ntype BaseOptions<I, S, E, R, ER, D> = {\n readonly runtime: Runtime<R, ER>\n readonly transport: Transport.Transport<Input.Sent<I>, S, E, R>\n readonly initial: S\n readonly step: (state: S, envelope: D) => S\n readonly busy: Busy\n readonly isBusy?: (state: S) => boolean\n /** Optimistic update applied before the request goes out. */\n readonly submitted?: (state: S, input: Input.Sent<I>) => S\n /** Applied when a run's stream ends without its terminal. */\n readonly ended?: (state: S, reason: \"stopped\" | \"disconnected\") => S\n}\n\nexport type Options<I, S, E, R, ER, D = Envelope> = BaseOptions<I, S, E, R, ER, D> & WithDecode<D>\n\nconst identity: Decode<Envelope> = Effect.succeed\n\nconst build = <I, S, E, R, ER, D>(\n options: BaseOptions<I, S, E, R, ER, D> & { readonly decode: Decode<D> },\n) => {\n const state = Atom.make(options.initial).pipe(Atom.keepAlive)\n const streamId = Atom.make<StreamId | null>(null).pipe(Atom.keepAlive)\n\n const follow = (envelopes: Stream.Stream<Envelope, E, R>) =>\n envelopes.pipe(\n Stream.runForEach((envelope) =>\n options\n .decode(envelope)\n .pipe(\n Effect.flatMap((decoded) =>\n Atom.set(streamId, envelope.streamId).pipe(\n Effect.andThen(Atom.update(state, (s) => options.step(s, decoded))),\n ),\n ),\n ),\n ),\n Effect.onExit((exit) => {\n const ended = options.ended\n if (Exit.isSuccess(exit) || ended === undefined) return Effect.void\n const reason = Exit.hasInterrupts(exit) ? \"stopped\" : \"disconnected\"\n return Atom.update(state, (s) => ended(s, reason))\n }),\n )\n\n // Writing to a non-concurrent `Atom.fn` interrupts its previous run, so the\n // streaming lives in `run` and `submit` is a concurrent gate that applies\n // the busy policy before anything is interrupted. Every fn atom is kept\n // alive: an unobserved one is disposed, which interrupts its fiber.\n const run = options.runtime\n .fn<Input.Sent<I>>()((input) =>\n Effect.gen(function* () {\n const submitted = options.submitted\n if (submitted !== undefined) yield* Atom.update(state, (s) => submitted(s, input))\n yield* follow(options.transport.start(input, yield* Atom.get(state)))\n }),\n )\n .pipe(Atom.keepAlive)\n\n const submit = options.runtime\n .fn<Input.Draft<I>>()(\n (draft) =>\n Effect.gen(function* () {\n const blocked =\n options.busy === \"block\" && options.isBusy?.(yield* Atom.get(state)) === true\n if (blocked) return\n const input = yield* Input.withId(draft)\n yield* Atom.set(run, input)\n }),\n { concurrent: true },\n )\n .pipe(Atom.keepAlive)\n\n // Durable: ask the server (its `run-stopped` arrives on the stream).\n // Direct: interrupt the request; `ended` settles the run locally.\n const stop = options.runtime\n .fn<void>()(() =>\n Effect.gen(function* () {\n const id = yield* Atom.get(streamId)\n const serverStop = options.transport.stop\n if (serverStop !== undefined && id !== null) return yield* serverStop(id)\n yield* Atom.set(run, Atom.Interrupt)\n }),\n )\n .pipe(Atom.keepAlive)\n\n const resume = options.transport.resume\n const attach =\n resume === undefined\n ? undefined\n : options.runtime\n .fn<{ readonly streamId: StreamId; readonly after: number }>()((arg) =>\n follow(resume(arg.streamId, arg.after)),\n )\n .pipe(Atom.keepAlive)\n\n return {\n state,\n streamId,\n submit,\n /** The current run; `AtomRegistry.getResult` on it waits for the run to finish. */\n run,\n stop,\n /** Re-attach to a run (`{ streamId, after }`); set for durable transports. */\n resume: attach,\n }\n}\n\ntype Built<I, S, E, R, ER> = ReturnType<typeof build<I, S, E, R, ER, unknown>>\n\n/**\n * State over a per-run transport: `state`, `submit`, `stop`, `run`, and\n * `resume` when the transport can re-attach (`Transport.durable`,\n * `durableHttp`), typed as present for those.\n */\nexport function make<I, S, E, R, ER, D = Envelope>(\n options: Options<I, S, E, R, ER, D> & {\n readonly transport: Transport.Durable<Input.Sent<I>, S, E, R>\n },\n): Omit<Built<I, S, E, R, ER>, \"resume\"> & {\n readonly resume: NonNullable<Built<I, S, E, R, ER>[\"resume\"]>\n}\nexport function make<I, S, E, R, ER, D = Envelope>(\n options: Options<I, S, E, R, ER, D>,\n): Built<I, S, E, R, ER>\nexport function make<I, S, E, R, ER>(\n options: Options<I, S, E, R, ER, Envelope>,\n): Built<I, S, E, R, ER> {\n return build({ ...options, decode: options.decode ?? identity })\n}\n\ntype ThreadOptions<I, E, R, ER, P extends Payloads> = {\n readonly runtime: Runtime<R, ER>\n readonly transport: Transport.Transport<Input.Sent<I>, Thread.Thread<P>, E, R>\n readonly busy: Busy\n readonly initial?: Thread.Thread<P>\n}\n\n/**\n * `make` with the `Thread` preset: a message list plus the active run. Takes\n * the `Input` preset, with or without typed `data`. With `contract`, the\n * state is typed by it. For other entry types, spread a `Thread.make(config)`\n * fold into `make`.\n */\nexport function thread<I extends Input.Input, E, R, ER, P extends Payloads>(\n options: ThreadOptions<I, E, R, ER, P> & { readonly contract: UiContract<P> },\n): Built<I, Thread.Thread<P>, E, R, ER>\nexport function thread<I extends Input.Input, E, R, ER>(\n options: ThreadOptions<I, E, R, ER, Untyped>,\n): Built<I, Thread.Thread, E, R, ER>\n// The implementation sees `P` as `any`: the options both read and receive\n// `Thread<P>`, so no single `P` fits both overloads. Callers get the overloads.\nexport function thread<I extends Input.Input, E, R, ER>(\n options: ThreadOptions<I, E, R, ER, any> & { readonly contract?: UiContract<any> },\n): Built<I, Thread.Thread<any>, E, R, ER> {\n const fold = Thread.presetFor<any>()\n return build({\n runtime: options.runtime,\n transport: options.transport,\n busy: options.busy,\n initial: options.initial ?? fold.initial,\n step: fold.step,\n decode: options.contract?.decode ?? identity,\n isBusy: (t) => t.active !== null,\n submitted: (t, input: Input.Sent<I>) => fold.submitted(t, input),\n ended: fold.ended,\n })\n}\n\n// ---------------------------------------------------------------------------\n// Sessions: one stream carries every run (a durable chat)\n// ---------------------------------------------------------------------------\n\ntype BaseSessionOptions<I, S, E, R, ER, D> = {\n readonly runtime: Runtime<R, ER>\n readonly transport: Transport.Session<Input.Sent<I>, S, E, R>\n readonly initial: S\n readonly step: (state: S, envelope: D) => S\n /** While true, `submit` ignores new input (the server may also refuse). */\n readonly isBusy?: (state: S) => boolean\n /** Optimistic update applied before the input is sent. */\n readonly submitted?: (state: S, input: Input.Sent<I>) => S\n /** Applied when sending fails or the session's stream gives up. */\n readonly ended?: (state: S, reason: \"stopped\" | \"disconnected\") => S\n}\n\nexport type SessionOptions<I, S, E, R, ER, D = Envelope> = BaseSessionOptions<I, S, E, R, ER, D> &\n WithDecode<D>\n\nconst buildSession = <I, S, E, R, ER, D>(\n options: BaseSessionOptions<I, S, E, R, ER, D> & { readonly decode: Decode<D> },\n) => {\n const state = Atom.make(options.initial).pipe(Atom.keepAlive)\n const streamId = Atom.make<StreamId | null>(null).pipe(Atom.keepAlive)\n\n const settle = (exit: Exit.Exit<unknown, unknown>) => {\n const ended = options.ended\n return ended === undefined || Exit.isSuccess(exit) || Exit.hasInterrupts(exit)\n ? Effect.void\n : Atom.update(state, (s) => ended(s, \"disconnected\"))\n }\n\n // Non-concurrent: opening another session stops following the previous one.\n const open = options.runtime\n .fn<StreamId>()((id) =>\n Atom.set(streamId, id).pipe(\n Effect.andThen(\n options.transport\n .events(id, 0)\n .pipe(\n Stream.runForEach((envelope) =>\n options\n .decode(envelope)\n .pipe(\n Effect.flatMap((decoded) =>\n Atom.update(state, (s) => options.step(s, decoded)),\n ),\n ),\n ),\n ),\n ),\n Effect.onExit(settle),\n ),\n )\n .pipe(Atom.keepAlive)\n\n const submit = options.runtime\n .fn<Input.Draft<I>>()(\n (draft) =>\n Effect.gen(function* () {\n if (options.isBusy?.(yield* Atom.get(state)) === true) return\n const input = yield* Input.withId(draft)\n const submitted = options.submitted\n if (submitted !== undefined) yield* Atom.update(state, (s) => submitted(s, input))\n const opened = Option.fromNullishOr(yield* Atom.get(streamId))\n const sent = yield* options.transport.send(opened, input, yield* Atom.get(state))\n if (Option.isNone(opened)) yield* Atom.set(open, sent)\n }).pipe(Effect.onExit(settle)),\n { concurrent: true },\n )\n .pipe(Atom.keepAlive)\n\n // The server stops the active run; its `run-stopped` arrives on the stream.\n const stop = options.runtime\n .fn<void>()(() =>\n Effect.gen(function* () {\n const id = yield* Atom.get(streamId)\n const serverStop = options.transport.stop\n if (serverStop !== undefined && id !== null) yield* serverStop(id)\n }),\n )\n .pipe(Atom.keepAlive)\n\n return { state, streamId, open, submit, stop }\n}\n\ntype BuiltSession<I, S, E, R, ER> = ReturnType<typeof buildSession<I, S, E, R, ER, unknown>>\n\n/**\n * State over a session transport. `open(streamId)` follows a session's\n * stream from its start (e.g. a chat id kept across reloads). `submit` sends\n * an input; without an open session the server starts one, which is then\n * opened. Runs arrive on the followed stream, not as responses to `submit`.\n */\nexport function session<I, S, E, R, ER, D = Envelope>(\n options: SessionOptions<I, S, E, R, ER, D>,\n): BuiltSession<I, S, E, R, ER>\nexport function session<I, S, E, R, ER>(\n options: SessionOptions<I, S, E, R, ER, Envelope>,\n): BuiltSession<I, S, E, R, ER> {\n return buildSession({ ...options, decode: options.decode ?? identity })\n}\n\ntype SessionThreadOptions<I, E, R, ER, P extends Payloads> = {\n readonly runtime: Runtime<R, ER>\n readonly transport: Transport.Session<Input.Sent<I>, Thread.Thread<P>, E, R>\n readonly isBusy?: (thread: Thread.Thread<P>) => boolean\n}\n\n/**\n * `session` with the `Thread` preset. Input sent while a run is active, or\n * echoed by the server (`input-accepted`), waits in `thread.pending`: the\n * server queues it. Pass `isBusy` for a server that refuses instead. With\n * `contract`, the state is typed by it. For other entry types, spread a\n * `Thread.make(config)` fold into `session`.\n */\nexport function sessionThread<I extends Input.Input, E, R, ER, P extends Payloads>(\n options: SessionThreadOptions<I, E, R, ER, P> & { readonly contract: UiContract<P> },\n): BuiltSession<I, Thread.Thread<P>, E, R, ER>\nexport function sessionThread<I extends Input.Input, E, R, ER>(\n options: SessionThreadOptions<I, E, R, ER, Untyped>,\n): BuiltSession<I, Thread.Thread, E, R, ER>\n// `any` for `P` in the implementation only, as in `thread`.\nexport function sessionThread<I extends Input.Input, E, R, ER>(\n options: SessionThreadOptions<I, E, R, ER, any> & { readonly contract?: UiContract<any> },\n): BuiltSession<I, Thread.Thread<any>, E, R, ER> {\n const fold = Thread.presetFor<any>()\n return buildSession({\n runtime: options.runtime,\n transport: options.transport,\n initial: fold.initial,\n step: fold.step,\n decode: options.contract?.decode ?? identity,\n ...(options.isBusy !== undefined ? { isBusy: options.isBusy } : {}),\n submitted: (t, input: Input.Sent<I>) => fold.submitted(t, input),\n ended: fold.ended,\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Atoms.mjs","names":["Input.withId","Thread.presetFor"],"sources":["../src/Atoms.ts"],"sourcesContent":["/**\n * Framework-neutral state over a transport, built on `effect/reactivity`.\n * Per-run transports: `make` takes any fold (`initial` + `step`), `thread`\n * is the `Thread` preset. Session transports: `session` and\n * `sessionThread`, likewise. React reads them with `@effect/atom-react` (`useAtomValue`,\n * `useAtomSet`), Vue and Solid with their official bindings, plain JS with\n * `AtomRegistry.subscribe`.\n *\n * With a `UiContract`, every envelope is decoded once on arrival, so the\n * state holds decoded payloads and is typed by the contract.\n */\nimport { Effect, Exit, Option, type Schema, Stream } from \"effect\"\nimport type { SecureRandom } from \"effect/crypto\"\nimport { Atom } from \"effect/reactivity\"\nimport * as Input from \"./Input.js\"\nimport type { Envelope, Payloads, StreamId, Untyped } from \"./RunEvent.js\"\nimport * as Thread from \"./Thread.js\"\nimport type * as Transport from \"./Transport.js\"\nimport type { UiContract } from \"./UiContract.js\"\n\n/**\n * What `submit` does while a run is in flight. `\"block\"` ignores the new\n * input; `\"stop-then-submit\"` abandons the current run locally and starts\n * the new one (in durable mode the server-side run keeps going).\n */\nexport type Busy = \"block\" | \"stop-then-submit\"\n\n/**\n * The atoms' runtime: the transport's services plus `SecureRandom` for\n * input ids, e.g. `Atom.runtime(Layer.mergeAll(FetchHttpClient.layer,\n * SecureRandom.layerWebCrypto))`. A test provides a seeded one.\n */\nexport type Runtime<R, ER> = Atom.AtomRuntime<R | SecureRandom.SecureRandom, ER>\n\n/** Turns a wire envelope into what `step` folds (a contract's `decode`). */\nexport type Decode<D> = (envelope: Envelope) => Effect.Effect<D, Schema.SchemaError>\n\n// `decode` may be left out only when `step` folds wire envelopes.\ntype WithDecode<D> = [Envelope] extends [D]\n ? { readonly decode?: Decode<D> }\n : { readonly decode: Decode<D> }\n\n/**\n * `I` is the app's input type. `submit` takes a draft of it (`id` optional);\n * the transport receives it with the id assigned (`Input.Sent<I>`, a UUIDv7\n * from `SecureRandom`). `D` is the envelope `step` folds: the wire envelope,\n * or a contract's decoded one (then pass `decode`).\n */\ntype BaseOptions<I, S, E, R, ER, D> = {\n readonly runtime: Runtime<R, ER>\n readonly transport: Transport.Transport<Input.Sent<I>, S, E, R>\n readonly initial: S\n readonly step: (state: S, envelope: D) => S\n readonly busy: Busy\n readonly isBusy?: (state: S) => boolean\n /** Optimistic update applied before the request goes out. */\n readonly submitted?: (state: S, input: Input.Sent<I>) => S\n /** Applied when a run's stream ends without its terminal. */\n readonly ended?: (state: S, reason: \"stopped\" | \"disconnected\") => S\n}\n\nexport type Options<I, S, E, R, ER, D = Envelope> = BaseOptions<I, S, E, R, ER, D> & WithDecode<D>\n\nconst identity: Decode<Envelope> = Effect.succeed\n\nconst build = <I, S, E, R, ER, D>(\n options: BaseOptions<I, S, E, R, ER, D> & { readonly decode: Decode<D> },\n) => {\n const state = Atom.make(options.initial).pipe(Atom.keepAlive)\n const streamId = Atom.make<StreamId | null>(null).pipe(Atom.keepAlive)\n\n const follow = (envelopes: Stream.Stream<Envelope, E, R>) =>\n envelopes.pipe(\n Stream.runForEach((envelope) =>\n options\n .decode(envelope)\n .pipe(\n Effect.flatMap((decoded) =>\n Atom.set(streamId, envelope.streamId).pipe(\n Effect.andThen(Atom.update(state, (s) => options.step(s, decoded))),\n ),\n ),\n ),\n ),\n Effect.onExit((exit) => {\n const ended = options.ended\n if (Exit.isSuccess(exit) || ended === undefined) return Effect.void\n const reason = Exit.hasInterrupts(exit) ? \"stopped\" : \"disconnected\"\n return Atom.update(state, (s) => ended(s, reason))\n }),\n )\n\n // Writing to a non-concurrent `Atom.fn` interrupts its previous run, so the\n // streaming lives in `run` and `submit` is a concurrent gate that applies\n // the busy policy before anything is interrupted. Every fn atom is kept\n // alive: an unobserved one is disposed, which interrupts its fiber.\n const run = options.runtime\n .fn<Input.Sent<I>>()((input) =>\n Effect.gen(function* () {\n const submitted = options.submitted\n if (submitted !== undefined) yield* Atom.update(state, (s) => submitted(s, input))\n yield* follow(options.transport.start(input, yield* Atom.get(state)))\n }),\n )\n .pipe(Atom.keepAlive)\n\n const submit = options.runtime\n .fn<Input.Draft<I>>()(\n (draft) =>\n Effect.gen(function* () {\n const blocked =\n options.busy === \"block\" && options.isBusy?.(yield* Atom.get(state)) === true\n if (blocked) return\n const input = yield* Input.withId(draft)\n yield* Atom.set(run, input)\n }),\n { concurrent: true },\n )\n .pipe(Atom.keepAlive)\n\n // Durable: ask the server (its `run-stopped` arrives on the stream).\n // Direct: interrupt the request; `ended` settles the run locally.\n const stop = options.runtime\n .fn<void>()(() =>\n Effect.gen(function* () {\n const id = yield* Atom.get(streamId)\n const serverStop = options.transport.stop\n if (serverStop !== undefined && id !== null) return yield* serverStop(id)\n yield* Atom.set(run, Atom.Interrupt)\n }),\n )\n .pipe(Atom.keepAlive)\n\n const resume = options.transport.resume\n const attach =\n resume === undefined\n ? undefined\n : options.runtime\n .fn<{ readonly streamId: StreamId; readonly after: number }>()((arg) =>\n follow(resume(arg.streamId, arg.after)),\n )\n .pipe(Atom.keepAlive)\n\n return {\n state,\n streamId,\n submit,\n /** The current run; `AtomRegistry.getResult` on it waits for the run to finish. */\n run,\n stop,\n /** Re-attach to a run (`{ streamId, after }`); set for durable transports. */\n resume: attach,\n }\n}\n\ntype Built<I, S, E, R, ER> = ReturnType<typeof build<I, S, E, R, ER, unknown>>\n\n/**\n * State over a per-run transport: `state`, `submit`, `stop`, `run`, and\n * `resume` when the transport can re-attach (`Transport.durable`,\n * `durableHttp`), typed as present for those.\n */\nexport function make<I, S, E, R, ER, D = Envelope>(\n options: Options<I, S, E, R, ER, D> & {\n readonly transport: Transport.Durable<Input.Sent<I>, S, E, R>\n },\n): Omit<Built<I, S, E, R, ER>, \"resume\"> & {\n readonly resume: NonNullable<Built<I, S, E, R, ER>[\"resume\"]>\n}\nexport function make<I, S, E, R, ER, D = Envelope>(\n options: Options<I, S, E, R, ER, D>,\n): Built<I, S, E, R, ER>\nexport function make<I, S, E, R, ER>(\n options: Options<I, S, E, R, ER, Envelope>,\n): Built<I, S, E, R, ER> {\n return build({ ...options, decode: options.decode ?? identity })\n}\n\ntype ThreadOptions<I, E, R, ER, P extends Payloads> = {\n readonly runtime: Runtime<R, ER>\n readonly transport: Transport.Transport<Input.Sent<I>, Thread.Thread<P>, E, R>\n readonly busy: Busy\n readonly initial?: Thread.Thread<P>\n}\n\n/**\n * `make` with the `Thread` preset: a message list plus the active run. Takes\n * the `Input` preset, with or without typed `data`. With `contract`, the\n * state is typed by it. For other entry types, spread a `Thread.make(config)`\n * fold into `make`.\n */\nexport function thread<I extends Input.Input, E, R, ER, P extends Payloads>(\n options: ThreadOptions<I, E, R, ER, P> & { readonly contract: UiContract<P> },\n): Built<I, Thread.Thread<P>, E, R, ER>\nexport function thread<I extends Input.Input, E, R, ER>(\n options: ThreadOptions<I, E, R, ER, Untyped>,\n): Built<I, Thread.Thread, E, R, ER>\n// The implementation sees `P` as `any`: the options both read and receive\n// `Thread<P>`, so no single `P` fits both overloads. Callers get the overloads.\nexport function thread<I extends Input.Input, E, R, ER>(\n options: ThreadOptions<I, E, R, ER, any> & { readonly contract?: UiContract<any> },\n): Built<I, Thread.Thread<any>, E, R, ER> {\n const fold = Thread.presetFor<any>()\n return build({\n runtime: options.runtime,\n transport: options.transport,\n busy: options.busy,\n initial: options.initial ?? fold.initial,\n step: fold.step,\n decode: options.contract?.decode ?? identity,\n isBusy: (t) => t.active !== null,\n submitted: (t, input: Input.Sent<I>) => fold.submitted(t, input),\n ended: fold.ended,\n })\n}\n\n// ---------------------------------------------------------------------------\n// Sessions: one stream carries every run (a durable chat)\n// ---------------------------------------------------------------------------\n\ntype BaseSessionOptions<I, S, E, R, ER, D> = {\n readonly runtime: Runtime<R, ER>\n readonly transport: Transport.Session<Input.Sent<I>, S, E, R>\n readonly initial: S\n readonly step: (state: S, envelope: D) => S\n /** While true, `submit` ignores new input (the server may also refuse). */\n readonly isBusy?: (state: S) => boolean\n /** Optimistic update applied before the input is sent. */\n readonly submitted?: (state: S, input: Input.Sent<I>) => S\n /** Applied when sending fails or the session's stream gives up. */\n readonly ended?: (state: S, reason: \"stopped\" | \"disconnected\") => S\n}\n\nexport type SessionOptions<I, S, E, R, ER, D = Envelope> = BaseSessionOptions<I, S, E, R, ER, D> &\n WithDecode<D>\n\n/** What `open` follows: a session from its start, or from `after` (e.g. a hydrated thread's `lastSeq`). */\nexport type Open = StreamId | { readonly streamId: StreamId; readonly after: number }\n\nconst buildSession = <I, S, E, R, ER, D>(\n options: BaseSessionOptions<I, S, E, R, ER, D> & { readonly decode: Decode<D> },\n) => {\n const state = Atom.make(options.initial).pipe(Atom.keepAlive)\n const streamId = Atom.make<StreamId | null>(null).pipe(Atom.keepAlive)\n\n const settle = (exit: Exit.Exit<unknown, unknown>) => {\n const ended = options.ended\n return ended === undefined || Exit.isSuccess(exit) || Exit.hasInterrupts(exit)\n ? Effect.void\n : Atom.update(state, (s) => ended(s, \"disconnected\"))\n }\n\n // Non-concurrent: opening another session stops following the previous one.\n const open = options.runtime\n .fn<Open>()((target) => {\n const { streamId: id, after } =\n typeof target === \"string\" ? { streamId: target, after: 0 } : target\n return Atom.set(streamId, id).pipe(\n Effect.andThen(\n options.transport\n .events(id, after)\n .pipe(\n Stream.runForEach((envelope) =>\n options\n .decode(envelope)\n .pipe(\n Effect.flatMap((decoded) =>\n Atom.update(state, (s) => options.step(s, decoded)),\n ),\n ),\n ),\n ),\n ),\n Effect.onExit(settle),\n )\n })\n .pipe(Atom.keepAlive)\n\n const submit = options.runtime\n .fn<Input.Draft<I>>()(\n (draft) =>\n Effect.gen(function* () {\n if (options.isBusy?.(yield* Atom.get(state)) === true) return\n const input = yield* Input.withId(draft)\n const submitted = options.submitted\n if (submitted !== undefined) yield* Atom.update(state, (s) => submitted(s, input))\n const opened = Option.fromNullishOr(yield* Atom.get(streamId))\n const sent = yield* options.transport.send(opened, input, yield* Atom.get(state))\n if (Option.isNone(opened)) yield* Atom.set(open, sent)\n }).pipe(Effect.onExit(settle)),\n { concurrent: true },\n )\n .pipe(Atom.keepAlive)\n\n // The server stops the active run; its `run-stopped` arrives on the stream.\n const stop = options.runtime\n .fn<void>()(() =>\n Effect.gen(function* () {\n const id = yield* Atom.get(streamId)\n const serverStop = options.transport.stop\n if (serverStop !== undefined && id !== null) yield* serverStop(id)\n }),\n )\n .pipe(Atom.keepAlive)\n\n return { state, streamId, open, submit, stop }\n}\n\ntype BuiltSession<I, S, E, R, ER> = ReturnType<typeof buildSession<I, S, E, R, ER, unknown>>\n\n/**\n * State over a session transport. `open(streamId)` follows a session's\n * stream from its start (e.g. a chat id kept across reloads);\n * `open({ streamId, after })` from a position, for state hydrated elsewhere\n * (pass it as `initial`; `initial` never opens anything). `submit` sends\n * an input; without an open session the server starts one, which is then\n * opened. Runs arrive on the followed stream, not as responses to `submit`.\n */\nexport function session<I, S, E, R, ER, D = Envelope>(\n options: SessionOptions<I, S, E, R, ER, D>,\n): BuiltSession<I, S, E, R, ER>\nexport function session<I, S, E, R, ER>(\n options: SessionOptions<I, S, E, R, ER, Envelope>,\n): BuiltSession<I, S, E, R, ER> {\n return buildSession({ ...options, decode: options.decode ?? identity })\n}\n\ntype SessionThreadOptions<I, E, R, ER, P extends Payloads> = {\n readonly runtime: Runtime<R, ER>\n readonly transport: Transport.Session<Input.Sent<I>, Thread.Thread<P>, E, R>\n readonly isBusy?: (thread: Thread.Thread<P>) => boolean\n /** A thread hydrated elsewhere (e.g. from a database); follow it with `open({ streamId, after: lastSeq })`. */\n readonly initial?: Thread.Thread<P>\n}\n\n/**\n * `session` with the `Thread` preset. Input sent while a run is active, or\n * echoed by the server (`input-accepted`), waits in `thread.pending`: the\n * server queues it. Pass `isBusy` for a server that refuses instead. With\n * `contract`, the state is typed by it. For other entry types, spread a\n * `Thread.make(config)` fold into `session`.\n */\nexport function sessionThread<I extends Input.Input, E, R, ER, P extends Payloads>(\n options: SessionThreadOptions<I, E, R, ER, P> & { readonly contract: UiContract<P> },\n): BuiltSession<I, Thread.Thread<P>, E, R, ER>\nexport function sessionThread<I extends Input.Input, E, R, ER>(\n options: SessionThreadOptions<I, E, R, ER, Untyped>,\n): BuiltSession<I, Thread.Thread, E, R, ER>\n// `any` for `P` in the implementation only, as in `thread`.\nexport function sessionThread<I extends Input.Input, E, R, ER>(\n options: SessionThreadOptions<I, E, R, ER, any> & { readonly contract?: UiContract<any> },\n): BuiltSession<I, Thread.Thread<any>, E, R, ER> {\n const fold = Thread.presetFor<any>()\n return buildSession({\n runtime: options.runtime,\n transport: options.transport,\n initial: options.initial ?? fold.initial,\n step: fold.step,\n decode: options.contract?.decode ?? identity,\n ...(options.isBusy !== undefined ? { isBusy: options.isBusy } : {}),\n submitted: (t, input: Input.Sent<I>) => fold.submitted(t, input),\n ended: fold.ended,\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA+DA,MAAM,WAA6B,OAAO;AAE1C,MAAM,SACJ,YACG;CACH,MAAM,QAAQ,KAAK,KAAK,QAAQ,OAAO,CAAC,CAAC,KAAK,KAAK,SAAS;CAC5D,MAAM,WAAW,KAAK,KAAsB,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS;CAErE,MAAM,UAAU,cACd,UAAU,KACR,OAAO,YAAY,aACjB,QACG,OAAO,QAAQ,CAAC,CAChB,KACC,OAAO,SAAS,YACd,KAAK,IAAI,UAAU,SAAS,QAAQ,CAAC,CAAC,KACpC,OAAO,QAAQ,KAAK,OAAO,QAAQ,MAAM,QAAQ,KAAK,GAAG,OAAO,CAAC,CAAC,CACpE,CACF,CACF,CACJ,GACA,OAAO,QAAQ,SAAS;EACtB,MAAM,QAAQ,QAAQ;EACtB,IAAI,KAAK,UAAU,IAAI,KAAK,UAAU,KAAA,GAAW,OAAO,OAAO;EAC/D,MAAM,SAAS,KAAK,cAAc,IAAI,IAAI,YAAY;EACtD,OAAO,KAAK,OAAO,QAAQ,MAAM,MAAM,GAAG,MAAM,CAAC;CACnD,CAAC,CACH;CAMF,MAAM,MAAM,QAAQ,QACjB,GAAkB,CAAC,EAAE,UACpB,OAAO,IAAI,aAAa;EACtB,MAAM,YAAY,QAAQ;EAC1B,IAAI,cAAc,KAAA,GAAW,OAAO,KAAK,OAAO,QAAQ,MAAM,UAAU,GAAG,KAAK,CAAC;EACjF,OAAO,OAAO,QAAQ,UAAU,MAAM,OAAO,OAAO,KAAK,IAAI,KAAK,CAAC,CAAC;CACtE,CAAC,CACH,CAAC,CACA,KAAK,KAAK,SAAS;CAEtB,MAAM,SAAS,QAAQ,QACpB,GAAmB,CAAC,EAClB,UACC,OAAO,IAAI,aAAa;EAGtB,IADE,QAAQ,SAAS,WAAW,QAAQ,SAAS,OAAO,KAAK,IAAI,KAAK,CAAC,MAAM,MAC9D;EACb,MAAM,QAAQ,OAAOA,OAAa,KAAK;EACvC,OAAO,KAAK,IAAI,KAAK,KAAK;CAC5B,CAAC,GACH,EAAE,YAAY,KAAK,CACrB,CAAC,CACA,KAAK,KAAK,SAAS;CAItB,MAAM,OAAO,QAAQ,QAClB,GAAS,CAAC,OACT,OAAO,IAAI,aAAa;EACtB,MAAM,KAAK,OAAO,KAAK,IAAI,QAAQ;EACnC,MAAM,aAAa,QAAQ,UAAU;EACrC,IAAI,eAAe,KAAA,KAAa,OAAO,MAAM,OAAO,OAAO,WAAW,EAAE;EACxE,OAAO,KAAK,IAAI,KAAK,KAAK,SAAS;CACrC,CAAC,CACH,CAAC,CACA,KAAK,KAAK,SAAS;CAEtB,MAAM,SAAS,QAAQ,UAAU;CAUjC,OAAO;EACL;EACA;EACA;;EAEA;EACA;;EAEA,QAhBA,WAAW,KAAA,IACP,KAAA,IACA,QAAQ,QACL,GAA4D,CAAC,EAAE,QAC9D,OAAO,OAAO,IAAI,UAAU,IAAI,KAAK,CAAC,CACxC,CAAC,CACA,KAAK,KAAK,SAAS;CAW5B;AACF;AAmBA,SAAgB,KACd,SACuB;CACvB,OAAO,MAAM;EAAE,GAAG;EAAS,QAAQ,QAAQ,UAAU;CAAS,CAAC;AACjE;AAuBA,SAAgB,OACd,SACwC;CACxC,MAAM,OAAOC,UAAsB;CACnC,OAAO,MAAM;EACX,SAAS,QAAQ;EACjB,WAAW,QAAQ;EACnB,MAAM,QAAQ;EACd,SAAS,QAAQ,WAAW,KAAK;EACjC,MAAM,KAAK;EACX,QAAQ,QAAQ,UAAU,UAAU;EACpC,SAAS,MAAM,EAAE,WAAW;EAC5B,YAAY,GAAG,UAAyB,KAAK,UAAU,GAAG,KAAK;EAC/D,OAAO,KAAK;CACd,CAAC;AACH;AAyBA,MAAM,gBACJ,YACG;CACH,MAAM,QAAQ,KAAK,KAAK,QAAQ,OAAO,CAAC,CAAC,KAAK,KAAK,SAAS;CAC5D,MAAM,WAAW,KAAK,KAAsB,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS;CAErE,MAAM,UAAU,SAAsC;EACpD,MAAM,QAAQ,QAAQ;EACtB,OAAO,UAAU,KAAA,KAAa,KAAK,UAAU,IAAI,KAAK,KAAK,cAAc,IAAI,IACzE,OAAO,OACP,KAAK,OAAO,QAAQ,MAAM,MAAM,GAAG,cAAc,CAAC;CACxD;CAGA,MAAM,OAAO,QAAQ,QAClB,GAAS,CAAC,EAAE,WAAW;EACtB,MAAM,EAAE,UAAU,IAAI,UACpB,OAAO,WAAW,WAAW;GAAE,UAAU;GAAQ,OAAO;EAAE,IAAI;EAChE,OAAO,KAAK,IAAI,UAAU,EAAE,CAAC,CAAC,KAC5B,OAAO,QACL,QAAQ,UACL,OAAO,IAAI,KAAK,CAAC,CACjB,KACC,OAAO,YAAY,aACjB,QACG,OAAO,QAAQ,CAAC,CAChB,KACC,OAAO,SAAS,YACd,KAAK,OAAO,QAAQ,MAAM,QAAQ,KAAK,GAAG,OAAO,CAAC,CACpD,CACF,CACJ,CACF,CACJ,GACA,OAAO,OAAO,MAAM,CACtB;CACF,CAAC,CAAC,CACD,KAAK,KAAK,SAAS;CA6BtB,OAAO;EAAE;EAAO;EAAU;EAAM,QA3BjB,QAAQ,QACpB,GAAmB,CAAC,EAClB,UACC,OAAO,IAAI,aAAa;GACtB,IAAI,QAAQ,SAAS,OAAO,KAAK,IAAI,KAAK,CAAC,MAAM,MAAM;GACvD,MAAM,QAAQ,OAAOD,OAAa,KAAK;GACvC,MAAM,YAAY,QAAQ;GAC1B,IAAI,cAAc,KAAA,GAAW,OAAO,KAAK,OAAO,QAAQ,MAAM,UAAU,GAAG,KAAK,CAAC;GACjF,MAAM,SAAS,OAAO,cAAc,OAAO,KAAK,IAAI,QAAQ,CAAC;GAC7D,MAAM,OAAO,OAAO,QAAQ,UAAU,KAAK,QAAQ,OAAO,OAAO,KAAK,IAAI,KAAK,CAAC;GAChF,IAAI,OAAO,OAAO,MAAM,GAAG,OAAO,KAAK,IAAI,MAAM,IAAI;EACvD,CAAC,CAAC,CAAC,KAAK,OAAO,OAAO,MAAM,CAAC,GAC/B,EAAE,YAAY,KAAK,CACrB,CAAC,CACA,KAAK,KAAK,SAawB;EAAG,MAV3B,QAAQ,QAClB,GAAS,CAAC,OACT,OAAO,IAAI,aAAa;GACtB,MAAM,KAAK,OAAO,KAAK,IAAI,QAAQ;GACnC,MAAM,aAAa,QAAQ,UAAU;GACrC,IAAI,eAAe,KAAA,KAAa,OAAO,MAAM,OAAO,WAAW,EAAE;EACnE,CAAC,CACH,CAAC,CACA,KAAK,KAAK,SAE8B;CAAE;AAC/C;AAeA,SAAgB,QACd,SAC8B;CAC9B,OAAO,aAAa;EAAE,GAAG;EAAS,QAAQ,QAAQ,UAAU;CAAS,CAAC;AACxE;AAwBA,SAAgB,cACd,SAC+C;CAC/C,MAAM,OAAOC,UAAsB;CACnC,OAAO,aAAa;EAClB,SAAS,QAAQ;EACjB,WAAW,QAAQ;EACnB,SAAS,QAAQ,WAAW,KAAK;EACjC,MAAM,KAAK;EACX,QAAQ,QAAQ,UAAU,UAAU;EACpC,GAAI,QAAQ,WAAW,KAAA,IAAY,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;EACjE,YAAY,GAAG,UAAyB,KAAK,UAAU,GAAG,KAAK;EAC/D,OAAO,KAAK;CACd,CAAC;AACH"}
|
|
@@ -203,17 +203,18 @@ declare const ndjson: (request: HttpClientRequest.HttpClientRequest) => Stream.S
|
|
|
203
203
|
* Keep events flowing across dropped connections: `connect(after)` is
|
|
204
204
|
* re-invoked with the last seen `seq` on failure, per `schedule`; duplicates
|
|
205
205
|
* are dropped. A connection that ends cleanly before the end counts as a
|
|
206
|
-
* failure (`Disconnected`), so it is retried too.
|
|
206
|
+
* failure (`Disconnected`), so it is retried too. Without `schedule` nothing
|
|
207
|
+
* is retried: an early end just fails with `Disconnected`.
|
|
207
208
|
*
|
|
208
209
|
* `until: "terminal"` (default) follows one run and completes after its
|
|
209
210
|
* terminal. `until: "never"` follows a session stream that carries many runs
|
|
210
211
|
* (a durable chat) and never completes on its own.
|
|
211
212
|
*/
|
|
212
|
-
declare const resumable: <E, R, X>(connect: (after: number) => Stream.Stream<Envelope, E, R>, options
|
|
213
|
+
declare const resumable: <E, R, X = never>(connect: (after: number) => Stream.Stream<Envelope, E, R>, options?: {
|
|
213
214
|
readonly from?: number;
|
|
214
215
|
readonly until?: "terminal" | "never";
|
|
215
|
-
readonly schedule
|
|
216
|
+
readonly schedule?: Schedule.Schedule<X, E | Disconnected>;
|
|
216
217
|
}) => Stream.Stream<Envelope, E | Disconnected, R>;
|
|
217
218
|
//#endregion
|
|
218
219
|
export { ndjson as a, execute as i, Client_d_exports as n, resumable as o, Disconnected as r, sse as s, ClientError as t };
|
|
219
|
-
//# sourceMappingURL=Client-
|
|
220
|
+
//# sourceMappingURL=Client-B2Eqo39q.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Client-
|
|
1
|
+
{"version":3,"file":"Client-B2Eqo39q.d.mts","names":[],"sources":["../src/Client.ts"],"mappings":";;;;;;;;;;;cAaa,qBAAqB;WAA4C;;;KAGlE,cACR,gBAAgB,kBAChB,OAAO,cACP,IAAI,QACJ,IAAI,WACJ,OAAO;;cAGE,UAAO,SAAa,kBAAkB,sBAAiB,OAAA,gDAAA,oBAAA,gBAAA,iBAAA,WAAA;;cASvD,MAAG,SAAa,kBAAkB,sBAAiB,OAAA,OAAA,UAAA,gBAAA,kBAAA,IAAA,QAAA,OAAA,cAAA,IAAA,UAAA,WAAA;;cAInD,SAAM,SAAa,kBAAkB,sBAAiB,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GACuB,gBAAA,kBAAA,OAAA,cAAA,OAAA,aAAA,WAAA;;;;;;;;;;;;cAa7E,YAAa,GAAG,GAAG,WAAS,UAC7B,kBAAkB,OAAO,OAAO,UAAU,GAAG,IAAE;WAE9C;WACA;WACA,WAAW,SAAS,SAAS,GAAG,IAAI;MAE9C,OAAO,OAAO,UAAU,IAAI,cAAc"}
|
package/dist/Client.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as ndjson, i as execute, o as resumable, r as Disconnected, s as sse, t as ClientError } from "./Client-
|
|
1
|
+
import { a as ndjson, i as execute, o as resumable, r as Disconnected, s as sse, t as ClientError } from "./Client-B2Eqo39q.mjs";
|
|
2
2
|
export { ClientError, Disconnected, execute, ndjson, resumable, sse };
|
package/dist/Client.mjs
CHANGED
|
@@ -31,23 +31,26 @@ const ndjson = (request) => body(request).pipe(Stream.pipeThroughChannelOrFail(N
|
|
|
31
31
|
* Keep events flowing across dropped connections: `connect(after)` is
|
|
32
32
|
* re-invoked with the last seen `seq` on failure, per `schedule`; duplicates
|
|
33
33
|
* are dropped. A connection that ends cleanly before the end counts as a
|
|
34
|
-
* failure (`Disconnected`), so it is retried too.
|
|
34
|
+
* failure (`Disconnected`), so it is retried too. Without `schedule` nothing
|
|
35
|
+
* is retried: an early end just fails with `Disconnected`.
|
|
35
36
|
*
|
|
36
37
|
* `until: "terminal"` (default) follows one run and completes after its
|
|
37
38
|
* terminal. `until: "never"` follows a session stream that carries many runs
|
|
38
39
|
* (a durable chat) and never completes on its own.
|
|
39
40
|
*/
|
|
40
41
|
const resumable = (connect, options) => Stream.unwrap(Effect.gen(function* () {
|
|
41
|
-
const untilTerminal = options
|
|
42
|
+
const untilTerminal = options?.until !== "never";
|
|
42
43
|
const ends = (envelope) => untilTerminal && isTerminal(envelope.event);
|
|
43
44
|
const position = yield* Ref.make({
|
|
44
|
-
seq: options
|
|
45
|
+
seq: options?.from ?? 0,
|
|
45
46
|
done: false
|
|
46
47
|
});
|
|
47
|
-
|
|
48
|
+
const attempt = Stream.unwrap(Ref.get(position).pipe(Effect.map(({ seq: after }) => connect(after).pipe(Stream.filterEffect((envelope) => Ref.modify(position, (p) => envelope.seq > p.seq ? [true, {
|
|
48
49
|
seq: envelope.seq,
|
|
49
50
|
done: ends(envelope)
|
|
50
|
-
}] : [false, p])), Stream.takeUntil(ends), Stream.concat(Stream.unwrap(Ref.get(position).pipe(Effect.map((p) => p.done ? Stream.empty : Stream.fail(new Disconnected({ lastSeq: p.seq }))))))))))
|
|
51
|
+
}] : [false, p])), Stream.takeUntil(ends), Stream.concat(Stream.unwrap(Ref.get(position).pipe(Effect.map((p) => p.done ? Stream.empty : Stream.fail(new Disconnected({ lastSeq: p.seq }))))))))));
|
|
52
|
+
const schedule = options?.schedule;
|
|
53
|
+
return schedule === void 0 ? attempt : attempt.pipe(Stream.retry(schedule));
|
|
51
54
|
}));
|
|
52
55
|
//#endregion
|
|
53
56
|
export { Disconnected, execute, ndjson, resumable, sse, Client_exports as t };
|
package/dist/Client.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Client.mjs","names":["SseEncoding.decode"],"sources":["../src/Client.ts"],"sourcesContent":["/**\n * Browser-safe sources of envelopes. Every source yields\n * `Stream<Envelope>`; `resumable` adds reconnect-from-last-seq on top of\n * any of them. HTTP goes through `effect/http`'s `HttpClient`, so a browser\n * provides `FetchHttpClient.layer` and tests provide a fake client.\n */\nimport { Data, Effect, Ref, type Schedule, type Schema, Stream } from \"effect\"\nimport { Ndjson, type Sse } from \"effect/encoding\"\nimport { HttpClient, type HttpClientError, type HttpClientRequest } from \"effect/http\"\nimport { type Envelope, EnvelopeJson, isTerminal } from \"./RunEvent.js\"\nimport * as SseEncoding from \"./SseEncoding.js\"\n\n/** The connection ended before the run's terminal event arrived. */\nexport class Disconnected extends Data.TaggedError(\"Disconnected\")<{ readonly lastSeq: number }> {}\n\n/** What an HTTP source can fail with: transport, a malformed SSE frame, or a malformed NDJSON line. */\nexport type ClientError =\n | HttpClientError.HttpClientError\n | Schema.SchemaError\n | Sse.Retry\n | Sse.SseError\n | Ndjson.NdjsonError\n\n/** Run a request, failing on a non-2xx status. */\nexport const execute = (request: HttpClientRequest.HttpClientRequest) =>\n HttpClient.HttpClient.pipe(\n Effect.flatMap((client) => HttpClient.filterStatusOk(client).execute(request)),\n )\n\nconst body = (request: HttpClientRequest.HttpClientRequest) =>\n Stream.unwrap(Effect.map(execute(request), (response) => response.stream))\n\n/** An SSE response (`text/event-stream`) as envelopes. */\nexport const sse = (request: HttpClientRequest.HttpClientRequest) =>\n body(request).pipe(SseEncoding.decode)\n\n/** An NDJSON response (one envelope per line) as envelopes. */\nexport const ndjson = (request: HttpClientRequest.HttpClientRequest) =>\n body(request).pipe(Stream.pipeThroughChannelOrFail(Ndjson.decodeSchema(EnvelopeJson)()))\n\n/**\n * Keep events flowing across dropped connections: `connect(after)` is\n * re-invoked with the last seen `seq` on failure, per `schedule`; duplicates\n * are dropped. A connection that ends cleanly before the end counts as a\n * failure (`Disconnected`), so it is retried too
|
|
1
|
+
{"version":3,"file":"Client.mjs","names":["SseEncoding.decode"],"sources":["../src/Client.ts"],"sourcesContent":["/**\n * Browser-safe sources of envelopes. Every source yields\n * `Stream<Envelope>`; `resumable` adds reconnect-from-last-seq on top of\n * any of them. HTTP goes through `effect/http`'s `HttpClient`, so a browser\n * provides `FetchHttpClient.layer` and tests provide a fake client.\n */\nimport { Data, Effect, Ref, type Schedule, type Schema, Stream } from \"effect\"\nimport { Ndjson, type Sse } from \"effect/encoding\"\nimport { HttpClient, type HttpClientError, type HttpClientRequest } from \"effect/http\"\nimport { type Envelope, EnvelopeJson, isTerminal } from \"./RunEvent.js\"\nimport * as SseEncoding from \"./SseEncoding.js\"\n\n/** The connection ended before the run's terminal event arrived. */\nexport class Disconnected extends Data.TaggedError(\"Disconnected\")<{ readonly lastSeq: number }> {}\n\n/** What an HTTP source can fail with: transport, a malformed SSE frame, or a malformed NDJSON line. */\nexport type ClientError =\n | HttpClientError.HttpClientError\n | Schema.SchemaError\n | Sse.Retry\n | Sse.SseError\n | Ndjson.NdjsonError\n\n/** Run a request, failing on a non-2xx status. */\nexport const execute = (request: HttpClientRequest.HttpClientRequest) =>\n HttpClient.HttpClient.pipe(\n Effect.flatMap((client) => HttpClient.filterStatusOk(client).execute(request)),\n )\n\nconst body = (request: HttpClientRequest.HttpClientRequest) =>\n Stream.unwrap(Effect.map(execute(request), (response) => response.stream))\n\n/** An SSE response (`text/event-stream`) as envelopes. */\nexport const sse = (request: HttpClientRequest.HttpClientRequest) =>\n body(request).pipe(SseEncoding.decode)\n\n/** An NDJSON response (one envelope per line) as envelopes. */\nexport const ndjson = (request: HttpClientRequest.HttpClientRequest) =>\n body(request).pipe(Stream.pipeThroughChannelOrFail(Ndjson.decodeSchema(EnvelopeJson)()))\n\n/**\n * Keep events flowing across dropped connections: `connect(after)` is\n * re-invoked with the last seen `seq` on failure, per `schedule`; duplicates\n * are dropped. A connection that ends cleanly before the end counts as a\n * failure (`Disconnected`), so it is retried too. Without `schedule` nothing\n * is retried: an early end just fails with `Disconnected`.\n *\n * `until: \"terminal\"` (default) follows one run and completes after its\n * terminal. `until: \"never\"` follows a session stream that carries many runs\n * (a durable chat) and never completes on its own.\n */\nexport const resumable = <E, R, X = never>(\n connect: (after: number) => Stream.Stream<Envelope, E, R>,\n options?: {\n readonly from?: number\n readonly until?: \"terminal\" | \"never\"\n readonly schedule?: Schedule.Schedule<X, E | Disconnected>\n },\n): Stream.Stream<Envelope, E | Disconnected, R> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const untilTerminal = options?.until !== \"never\"\n const ends = (envelope: Envelope) => untilTerminal && isTerminal(envelope.event)\n // The last seen seq, and whether the run's terminal arrived.\n const position = yield* Ref.make({ seq: options?.from ?? 0, done: false })\n const attempt = Stream.unwrap(\n Ref.get(position).pipe(\n Effect.map(({ seq: after }) =>\n connect(after).pipe(\n Stream.filterEffect((envelope) =>\n Ref.modify(position, (p) =>\n envelope.seq > p.seq\n ? [true, { seq: envelope.seq, done: ends(envelope) }]\n : [false, p],\n ),\n ),\n Stream.takeUntil(ends),\n Stream.concat(\n Stream.unwrap(\n Ref.get(position).pipe(\n Effect.map((p) =>\n p.done ? Stream.empty : Stream.fail(new Disconnected({ lastSeq: p.seq })),\n ),\n ),\n ),\n ),\n ),\n ),\n ),\n )\n const schedule = options?.schedule\n return schedule === undefined ? attempt : attempt.pipe(Stream.retry(schedule))\n }),\n )\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAaA,IAAa,eAAb,cAAkC,KAAK,YAAY,cAAc,CAAC,CAA+B,CAAC;;AAWlG,MAAa,WAAW,YACtB,WAAW,WAAW,KACpB,OAAO,SAAS,WAAW,WAAW,eAAe,MAAM,CAAC,CAAC,QAAQ,OAAO,CAAC,CAC/E;AAEF,MAAM,QAAQ,YACZ,OAAO,OAAO,OAAO,IAAI,QAAQ,OAAO,IAAI,aAAa,SAAS,MAAM,CAAC;;AAG3E,MAAa,OAAO,YAClB,KAAK,OAAO,CAAC,CAAC,KAAKA,MAAkB;;AAGvC,MAAa,UAAU,YACrB,KAAK,OAAO,CAAC,CAAC,KAAK,OAAO,yBAAyB,OAAO,aAAa,YAAY,CAAC,CAAC,CAAC,CAAC;;;;;;;;;;;;AAazF,MAAa,aACX,SACA,YAMA,OAAO,OACL,OAAO,IAAI,aAAa;CACtB,MAAM,gBAAgB,SAAS,UAAU;CACzC,MAAM,QAAQ,aAAuB,iBAAiB,WAAW,SAAS,KAAK;CAE/E,MAAM,WAAW,OAAO,IAAI,KAAK;EAAE,KAAK,SAAS,QAAQ;EAAG,MAAM;CAAM,CAAC;CACzE,MAAM,UAAU,OAAO,OACrB,IAAI,IAAI,QAAQ,CAAC,CAAC,KAChB,OAAO,KAAK,EAAE,KAAK,YACjB,QAAQ,KAAK,CAAC,CAAC,KACb,OAAO,cAAc,aACnB,IAAI,OAAO,WAAW,MACpB,SAAS,MAAM,EAAE,MACb,CAAC,MAAM;EAAE,KAAK,SAAS;EAAK,MAAM,KAAK,QAAQ;CAAE,CAAC,IAClD,CAAC,OAAO,CAAC,CACf,CACF,GACA,OAAO,UAAU,IAAI,GACrB,OAAO,OACL,OAAO,OACL,IAAI,IAAI,QAAQ,CAAC,CAAC,KAChB,OAAO,KAAK,MACV,EAAE,OAAO,OAAO,QAAQ,OAAO,KAAK,IAAI,aAAa,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAC1E,CACF,CACF,CACF,CACF,CACF,CACF,CACF;CACA,MAAM,WAAW,SAAS;CAC1B,OAAO,aAAa,KAAA,IAAY,UAAU,QAAQ,KAAK,OAAO,MAAM,QAAQ,CAAC;AAC/E,CAAC,CACH"}
|
package/dist/Run.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Envelope, InputId, RunEvent, RunId, StreamId } from "./RunEvent.mjs";
|
|
2
2
|
import { Cause, Effect, Schema, Stream } from "effect";
|
|
3
|
-
import * as Duration from "effect/Duration";
|
|
3
|
+
import * as Duration$1 from "effect/Duration";
|
|
4
4
|
declare namespace Run_d_exports {
|
|
5
5
|
export { SequenceState, StartOptions, coalesce, complete, failed, sequence, start, stopWhen };
|
|
6
6
|
}
|
|
@@ -38,7 +38,7 @@ declare const sequence: (streamId: StreamId, options?: {
|
|
|
38
38
|
* Bounds frames on the wire and rows in a durable log; adds up to `window`
|
|
39
39
|
* latency.
|
|
40
40
|
*/
|
|
41
|
-
declare const coalesce: (window: Duration.Input) => <E, R>(self: Stream.Stream<RunEvent, E, R>) => Stream.Stream<RunEvent, E, R>;
|
|
41
|
+
declare const coalesce: (window: Duration$1.Input) => <E, R>(self: Stream.Stream<RunEvent, E, R>) => Stream.Stream<RunEvent, E, R>;
|
|
42
42
|
//#endregion
|
|
43
43
|
export { SequenceState, StartOptions, coalesce, complete, failed, sequence, start, stopWhen, Run_d_exports as t };
|
|
44
44
|
//# sourceMappingURL=Run.d.mts.map
|
package/dist/Run.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Run.d.mts","names":[],"sources":["../src/Run.ts"],"mappings":";;;;;;KAmCY;WACD,OAAO;;WAEP,SAAS,cAAc;;WAEvB;;;cAIE,QAAK,SACN,kBACT,GAAG,GAAC,MAAQ,OAAO,OAAO,UAAU,GAAG,OAAK,OAAO,OAAO,UAAU,GAAG;;cAY7D,WAAY,GAAG,GAAC,MACrB,OAAO,OAAO,UAAU,GAAG,OAChC,OAAO,OAAO,UAAU,GAAG;;cAcjB,WAAQ,QACV,OAAO,kBACf,GAAG,GAAC,MAAQ,OAAO,OAAO,UAAU,GAAG,OAAK,OAAO,OAAO,UAAU,GAAG;;cAS7D,SAAM,OAAW,MAAM,mBAAiB;cAWxC,eAAa,OAAA;;;;;KAKd,uBAAuB,cAAc;;;;;cAiBpC,WAAQ,UACR,UAAQ;WAAuB;OACzC,GAAG,GAAC,MAAQ,OAAO,OAAO,UAAU,GAAG,OAAK,OAAO,OAAO,UAAU,GAAG;;;;;;;cAuC7D,WAAQ,QACV,
|
|
1
|
+
{"version":3,"file":"Run.d.mts","names":[],"sources":["../src/Run.ts"],"mappings":";;;;;;KAmCY;WACD,OAAO;;WAEP,SAAS,cAAc;;WAEvB;;;cAIE,QAAK,SACN,kBACT,GAAG,GAAC,MAAQ,OAAO,OAAO,UAAU,GAAG,OAAK,OAAO,OAAO,UAAU,GAAG;;cAY7D,WAAY,GAAG,GAAC,MACrB,OAAO,OAAO,UAAU,GAAG,OAChC,OAAO,OAAO,UAAU,GAAG;;cAcjB,WAAQ,QACV,OAAO,kBACf,GAAG,GAAC,MAAQ,OAAO,OAAO,UAAU,GAAG,OAAK,OAAO,OAAO,UAAU,GAAG;;cAS7D,SAAM,OAAW,MAAM,mBAAiB;cAWxC,eAAa,OAAA;;;;;KAKd,uBAAuB,cAAc;;;;;cAiBpC,WAAQ,UACR,UAAQ;WAAuB;OACzC,GAAG,GAAC,MAAQ,OAAO,OAAO,UAAU,GAAG,OAAK,OAAO,OAAO,UAAU,GAAG;;;;;;;cAuC7D,WAAQ,QACV,WAAS,WACjB,GAAG,GAAC,MAAQ,OAAO,OAAO,UAAU,GAAG,OAAK,OAAO,OAAO,UAAU,GAAG"}
|
package/dist/SseEncoding.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Envelope } from "./RunEvent.mjs";
|
|
2
|
-
import { Option, Schema, Stream } from "effect";
|
|
2
|
+
import { Duration, Option, Schema, Stream } from "effect";
|
|
3
3
|
import { Sse } from "effect/encoding";
|
|
4
4
|
declare namespace SseEncoding_d_exports {
|
|
5
|
-
export { EnvelopeEvent, decode, encode, lastEventId, responseHeaders };
|
|
5
|
+
export { EnvelopeEvent, decode, encode, heartbeat, lastEventId, responseHeaders };
|
|
6
6
|
}
|
|
7
7
|
/** An envelope as an SSE event: `id` = seq, `event` = type, `data` = the envelope as JSON. */
|
|
8
8
|
declare const EnvelopeEvent: Schema.decodeTo<Schema.toType<Schema.Struct<{
|
|
@@ -416,6 +416,13 @@ declare const EnvelopeEvent: Schema.decodeTo<Schema.toType<Schema.Struct<{
|
|
|
416
416
|
declare const encode: <E, R>(self: Stream.Stream<Envelope, E, R>) => Stream.Stream<Uint8Array, E | Schema.SchemaError, R>;
|
|
417
417
|
/** An SSE response body → envelopes. A `retry:` hint from the server fails with `Sse.Retry`. */
|
|
418
418
|
declare const decode: <E, R>(self: Stream.Stream<Uint8Array, E, R>) => Stream.Stream<Envelope, E | Sse.Retry | Sse.SseError | Schema.SchemaError, R>;
|
|
419
|
+
/**
|
|
420
|
+
* Interleave an SSE comment every `interval`, so proxies don't close a stream
|
|
421
|
+
* that sits idle (a session between answers). Pipe after `encode`: each frame
|
|
422
|
+
* is one chunk, so a comment always lands between whole frames. Parsers
|
|
423
|
+
* (`EventSource`, `decode`) ignore comments. Ends with the event stream.
|
|
424
|
+
*/
|
|
425
|
+
declare const heartbeat: (interval: Duration.Input) => <E, R>(self: Stream.Stream<Uint8Array, E, R>) => Stream.Stream<Uint8Array, E, R>;
|
|
419
426
|
/**
|
|
420
427
|
* Headers an SSE response needs. Behind nginx-style proxies, also send
|
|
421
428
|
* `x-accel-buffering: no` so frames are not buffered.
|
|
@@ -433,5 +440,5 @@ declare const lastEventId: (headers: Headers | {
|
|
|
433
440
|
readonly [key: string]: string | undefined;
|
|
434
441
|
}) => Option.Option<number>;
|
|
435
442
|
//#endregion
|
|
436
|
-
export { EnvelopeEvent, decode, encode, lastEventId, responseHeaders, SseEncoding_d_exports as t };
|
|
443
|
+
export { EnvelopeEvent, decode, encode, heartbeat, lastEventId, responseHeaders, SseEncoding_d_exports as t };
|
|
437
444
|
//# sourceMappingURL=SseEncoding.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SseEncoding.d.mts","names":[],"sources":["../src/SseEncoding.ts"],"mappings":";;;;;;;cAaa,eAAa,OAAA,SAAA,OAAA,OAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAqBb,SAAU,GAAG,GAAC,MACnB,OAAO,OAAO,UAAU,GAAG,OAChC,OAAO,OAAO,YAAY,IAAI,OAAO,aAAa;;cAWxC,SAAU,GAAG,GAAC,MACnB,OAAO,OAAO,YAAY,GAAG,OAClC,OAAO,OAAO,UAAU,IAAI,IAAI,QAAQ,IAAI,WAAW,OAAO,aAAa;;;;;
|
|
1
|
+
{"version":3,"file":"SseEncoding.d.mts","names":[],"sources":["../src/SseEncoding.ts"],"mappings":";;;;;;;cAaa,eAAa,OAAA,SAAA,OAAA,OAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAqBb,SAAU,GAAG,GAAC,MACnB,OAAO,OAAO,UAAU,GAAG,OAChC,OAAO,OAAO,YAAY,IAAI,OAAO,aAAa;;cAWxC,SAAU,GAAG,GAAC,MACnB,OAAO,OAAO,YAAY,GAAG,OAClC,OAAO,OAAO,UAAU,IAAI,IAAI,QAAQ,IAAI,WAAW,OAAO,aAAa;;;;;;;cAWjE,YAAS,UACT,SAAS,WACnB,GAAG,GAAC,MAAQ,OAAO,OAAO,YAAY,GAAG,OAAK,OAAO,OAAO,YAAY,GAAG;;;;;cAWjE,iBAAiB,SAAS;;;;;;;;;cAe1B,cAAW,SACb;YAAsB;MAC9B,OAAO"}
|
package/dist/SseEncoding.mjs
CHANGED
|
@@ -15,6 +15,7 @@ var SseEncoding_exports = /* @__PURE__ */ __exportAll({
|
|
|
15
15
|
EnvelopeEvent: () => EnvelopeEvent,
|
|
16
16
|
decode: () => decode,
|
|
17
17
|
encode: () => encode,
|
|
18
|
+
heartbeat: () => heartbeat,
|
|
18
19
|
lastEventId: () => lastEventId,
|
|
19
20
|
responseHeaders: () => responseHeaders
|
|
20
21
|
});
|
|
@@ -40,6 +41,14 @@ const encode = (self) => self.pipe(Stream.mapEffect((envelope) => encodeEvent(en
|
|
|
40
41
|
})))), Stream.encodeText);
|
|
41
42
|
/** An SSE response body → envelopes. A `retry:` hint from the server fails with `Sse.Retry`. */
|
|
42
43
|
const decode = (self) => self.pipe(Stream.decodeText, Stream.pipeThroughChannelOrFail(Sse.decodeSchema(EnvelopeEvent)));
|
|
44
|
+
const comment = new TextEncoder().encode(": heartbeat\n\n");
|
|
45
|
+
/**
|
|
46
|
+
* Interleave an SSE comment every `interval`, so proxies don't close a stream
|
|
47
|
+
* that sits idle (a session between answers). Pipe after `encode`: each frame
|
|
48
|
+
* is one chunk, so a comment always lands between whole frames. Parsers
|
|
49
|
+
* (`EventSource`, `decode`) ignore comments. Ends with the event stream.
|
|
50
|
+
*/
|
|
51
|
+
const heartbeat = (interval) => (self) => self.pipe(Stream.merge(Stream.tick(interval).pipe(Stream.map(() => comment)), { haltStrategy: "left" }));
|
|
43
52
|
/**
|
|
44
53
|
* Headers an SSE response needs. Behind nginx-style proxies, also send
|
|
45
54
|
* `x-accel-buffering: no` so frames are not buffered.
|
|
@@ -59,6 +68,6 @@ const parseSeq = Schema.decodeUnknownOption(Schema.FiniteFromString.check(Schema
|
|
|
59
68
|
*/
|
|
60
69
|
const lastEventId = (headers) => Option.fromNullishOr(headers instanceof Headers ? headers.get("last-event-id") : headers["last-event-id"]).pipe(Option.flatMap(parseSeq));
|
|
61
70
|
//#endregion
|
|
62
|
-
export { EnvelopeEvent, decode, encode, lastEventId, responseHeaders, SseEncoding_exports as t };
|
|
71
|
+
export { EnvelopeEvent, decode, encode, heartbeat, lastEventId, responseHeaders, SseEncoding_exports as t };
|
|
63
72
|
|
|
64
73
|
//# sourceMappingURL=SseEncoding.mjs.map
|
package/dist/SseEncoding.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SseEncoding.mjs","names":[],"sources":["../src/SseEncoding.ts"],"sourcesContent":["/**\n * SSE framing for envelopes, on Effect's `effect/encoding/Sse`. A frame\n * carries `id: <seq>` (so a browser's auto-reconnect sends `Last-Event-ID`),\n * `event: <type>` and the envelope's JSON in `data`. `EnvelopeEvent` is that\n * mapping as a Schema; `encode` / `decode` run it with Effect's SSE\n * encoder and parser.\n * For NDJSON use `effect/encoding/Ndjson` with `RunEvent.EnvelopeJson`.\n */\nimport { Effect, Option, Schema, SchemaTransformation, Stream } from \"effect\"\nimport { Sse } from \"effect/encoding\"\nimport { Envelope, EnvelopeJson } from \"./RunEvent.js\"\n\n/** An envelope as an SSE event: `id` = seq, `event` = type, `data` = the envelope as JSON. */\nexport const EnvelopeEvent = Schema.Struct({\n id: Schema.optional(Schema.String),\n event: Schema.String,\n data: Schema.fromJsonString(EnvelopeJson),\n}).pipe(\n Schema.decodeTo(\n Schema.toType(Envelope),\n SchemaTransformation.transform({\n decode: (event) => event.data,\n encode: (envelope) => ({\n id: String(envelope.seq),\n event: envelope.event.type,\n data: envelope,\n }),\n }),\n ),\n)\n\nconst encodeEvent = Schema.encodeEffect(EnvelopeEvent)\n\n/** Envelopes → an SSE response body. */\nexport const encode = <E, R>(\n self: Stream.Stream<Envelope, E, R>,\n): Stream.Stream<Uint8Array, E | Schema.SchemaError, R> =>\n self.pipe(\n Stream.mapEffect((envelope) =>\n encodeEvent(envelope).pipe(\n Effect.map((event) => Sse.encoder.write({ _tag: \"Event\", id: event.id, ...event })),\n ),\n ),\n Stream.encodeText,\n )\n\n/** An SSE response body → envelopes. A `retry:` hint from the server fails with `Sse.Retry`. */\nexport const decode = <E, R>(\n self: Stream.Stream<Uint8Array, E, R>,\n): Stream.Stream<Envelope, E | Sse.Retry | Sse.SseError | Schema.SchemaError, R> =>\n self.pipe(Stream.decodeText, Stream.pipeThroughChannelOrFail(Sse.decodeSchema(EnvelopeEvent)))\n\n/**\n * Headers an SSE response needs. Behind nginx-style proxies, also send\n * `x-accel-buffering: no` so frames are not buffered.\n */\nexport const responseHeaders: Readonly<Record<string, string>> = {\n \"content-type\": \"text/event-stream\",\n \"cache-control\": \"no-cache\",\n}\n\nconst parseSeq = Schema.decodeUnknownOption(Schema.FiniteFromString.check(Schema.isInt()))\n\n/**\n * The `seq` a reconnecting SSE client last received, from the standard\n * `Last-Event-ID` header (a browser's EventSource sends it on every\n * auto-reconnect). Accepts web `Headers` or Effect's record headers.\n *\n * Prefer it over any position the app carries in the URL: EventSource keeps\n * reconnecting to its original URL, so a URL position goes stale.\n */\nexport const lastEventId = (\n headers: Headers | { readonly [key: string]: string | undefined },\n): Option.Option<number> =>\n Option.fromNullishOr(\n headers instanceof Headers ? headers.get(\"last-event-id\") : headers[\"last-event-id\"],\n ).pipe(Option.flatMap(parseSeq))\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"SseEncoding.mjs","names":[],"sources":["../src/SseEncoding.ts"],"sourcesContent":["/**\n * SSE framing for envelopes, on Effect's `effect/encoding/Sse`. A frame\n * carries `id: <seq>` (so a browser's auto-reconnect sends `Last-Event-ID`),\n * `event: <type>` and the envelope's JSON in `data`. `EnvelopeEvent` is that\n * mapping as a Schema; `encode` / `decode` run it with Effect's SSE\n * encoder and parser.\n * For NDJSON use `effect/encoding/Ndjson` with `RunEvent.EnvelopeJson`.\n */\nimport { type Duration, Effect, Option, Schema, SchemaTransformation, Stream } from \"effect\"\nimport { Sse } from \"effect/encoding\"\nimport { Envelope, EnvelopeJson } from \"./RunEvent.js\"\n\n/** An envelope as an SSE event: `id` = seq, `event` = type, `data` = the envelope as JSON. */\nexport const EnvelopeEvent = Schema.Struct({\n id: Schema.optional(Schema.String),\n event: Schema.String,\n data: Schema.fromJsonString(EnvelopeJson),\n}).pipe(\n Schema.decodeTo(\n Schema.toType(Envelope),\n SchemaTransformation.transform({\n decode: (event) => event.data,\n encode: (envelope) => ({\n id: String(envelope.seq),\n event: envelope.event.type,\n data: envelope,\n }),\n }),\n ),\n)\n\nconst encodeEvent = Schema.encodeEffect(EnvelopeEvent)\n\n/** Envelopes → an SSE response body. */\nexport const encode = <E, R>(\n self: Stream.Stream<Envelope, E, R>,\n): Stream.Stream<Uint8Array, E | Schema.SchemaError, R> =>\n self.pipe(\n Stream.mapEffect((envelope) =>\n encodeEvent(envelope).pipe(\n Effect.map((event) => Sse.encoder.write({ _tag: \"Event\", id: event.id, ...event })),\n ),\n ),\n Stream.encodeText,\n )\n\n/** An SSE response body → envelopes. A `retry:` hint from the server fails with `Sse.Retry`. */\nexport const decode = <E, R>(\n self: Stream.Stream<Uint8Array, E, R>,\n): Stream.Stream<Envelope, E | Sse.Retry | Sse.SseError | Schema.SchemaError, R> =>\n self.pipe(Stream.decodeText, Stream.pipeThroughChannelOrFail(Sse.decodeSchema(EnvelopeEvent)))\n\nconst comment = new TextEncoder().encode(\": heartbeat\\n\\n\")\n\n/**\n * Interleave an SSE comment every `interval`, so proxies don't close a stream\n * that sits idle (a session between answers). Pipe after `encode`: each frame\n * is one chunk, so a comment always lands between whole frames. Parsers\n * (`EventSource`, `decode`) ignore comments. Ends with the event stream.\n */\nexport const heartbeat =\n (interval: Duration.Input) =>\n <E, R>(self: Stream.Stream<Uint8Array, E, R>): Stream.Stream<Uint8Array, E, R> =>\n self.pipe(\n Stream.merge(Stream.tick(interval).pipe(Stream.map(() => comment)), {\n haltStrategy: \"left\",\n }),\n )\n\n/**\n * Headers an SSE response needs. Behind nginx-style proxies, also send\n * `x-accel-buffering: no` so frames are not buffered.\n */\nexport const responseHeaders: Readonly<Record<string, string>> = {\n \"content-type\": \"text/event-stream\",\n \"cache-control\": \"no-cache\",\n}\n\nconst parseSeq = Schema.decodeUnknownOption(Schema.FiniteFromString.check(Schema.isInt()))\n\n/**\n * The `seq` a reconnecting SSE client last received, from the standard\n * `Last-Event-ID` header (a browser's EventSource sends it on every\n * auto-reconnect). Accepts web `Headers` or Effect's record headers.\n *\n * Prefer it over any position the app carries in the URL: EventSource keeps\n * reconnecting to its original URL, so a URL position goes stale.\n */\nexport const lastEventId = (\n headers: Headers | { readonly [key: string]: string | undefined },\n): Option.Option<number> =>\n Option.fromNullishOr(\n headers instanceof Headers ? headers.get(\"last-event-id\") : headers[\"last-event-id\"],\n ).pipe(Option.flatMap(parseSeq))\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAaA,MAAa,gBAAgB,OAAO,OAAO;CACzC,IAAI,OAAO,SAAS,OAAO,MAAM;CACjC,OAAO,OAAO;CACd,MAAM,OAAO,eAAe,YAAY;AAC1C,CAAC,CAAC,CAAC,KACD,OAAO,SACL,OAAO,OAAO,QAAQ,GACtB,qBAAqB,UAAU;CAC7B,SAAS,UAAU,MAAM;CACzB,SAAS,cAAc;EACrB,IAAI,OAAO,SAAS,GAAG;EACvB,OAAO,SAAS,MAAM;EACtB,MAAM;CACR;AACF,CAAC,CACH,CACF;AAEA,MAAM,cAAc,OAAO,aAAa,aAAa;;AAGrD,MAAa,UACX,SAEA,KAAK,KACH,OAAO,WAAW,aAChB,YAAY,QAAQ,CAAC,CAAC,KACpB,OAAO,KAAK,UAAU,IAAI,QAAQ,MAAM;CAAE,MAAM;CAAS,IAAI,MAAM;CAAI,GAAG;AAAM,CAAC,CAAC,CACpF,CACF,GACA,OAAO,UACT;;AAGF,MAAa,UACX,SAEA,KAAK,KAAK,OAAO,YAAY,OAAO,yBAAyB,IAAI,aAAa,aAAa,CAAC,CAAC;AAE/F,MAAM,UAAU,IAAI,YAAY,CAAC,CAAC,OAAO,iBAAiB;;;;;;;AAQ1D,MAAa,aACV,cACM,SACL,KAAK,KACH,OAAO,MAAM,OAAO,KAAK,QAAQ,CAAC,CAAC,KAAK,OAAO,UAAU,OAAO,CAAC,GAAG,EAClE,cAAc,OAChB,CAAC,CACH;;;;;AAMJ,MAAa,kBAAoD;CAC/D,gBAAgB;CAChB,iBAAiB;AACnB;AAEA,MAAM,WAAW,OAAO,oBAAoB,OAAO,iBAAiB,MAAM,OAAO,MAAM,CAAC,CAAC;;;;;;;;;AAUzF,MAAa,eACX,YAEA,OAAO,cACL,mBAAmB,UAAU,QAAQ,IAAI,eAAe,IAAI,QAAQ,gBACtE,CAAC,CAAC,KAAK,OAAO,QAAQ,QAAQ,CAAC"}
|
package/dist/Transport.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Envelope, RunEvent, StreamId } from "./RunEvent.mjs";
|
|
2
|
-
import { r as Disconnected, t as ClientError } from "./Client-
|
|
3
|
-
import { Effect, Option, Schedule, Stream } from "effect";
|
|
2
|
+
import { r as Disconnected, t as ClientError } from "./Client-B2Eqo39q.mjs";
|
|
3
|
+
import { Effect, Option, Predicate, Schedule, Stream } from "effect";
|
|
4
4
|
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
5
5
|
declare namespace Transport_d_exports {
|
|
6
|
-
export { Durable, HttpError, Session, Transport, direct, durable, durableHttp, local, session, sessionHttp };
|
|
6
|
+
export { Durable, ErrorOf, HttpError, ReconnectOptions, Reconnected, Resumable, Session, Transport, direct, durable, durableHttp, local, reconnect, session, sessionHttp };
|
|
7
7
|
}
|
|
8
8
|
type Transport<I, S, E, R> = {
|
|
9
9
|
/** Start a run for `input` and stream its envelopes. Interrupting the stream abandons it. */
|
|
@@ -32,38 +32,44 @@ declare const direct: <I, S, EB = never, RB = never>(options: {
|
|
|
32
32
|
declare const local: <I, S, E, R>(run: (input: I, state: S) => Stream.Stream<RunEvent, E, R>, options?: {
|
|
33
33
|
readonly streamId?: StreamId;
|
|
34
34
|
}) => Transport<I, S, E, R>;
|
|
35
|
-
/** A per-run transport that can re-attach to a run (durable). */
|
|
35
|
+
/** A per-run transport that can re-attach to a run (durable). `start` is `submit`, then `resume` from 0. */
|
|
36
36
|
type Durable<I, S, E, R> = Transport<I, S, E, R> & {
|
|
37
|
+
/** Start a run on the server and return its stream id. */
|
|
38
|
+
readonly submit: (input: I, state: S) => Effect.Effect<StreamId, E, R>;
|
|
37
39
|
readonly resume: (streamId: StreamId, after: number) => Stream.Stream<Envelope, E, R>;
|
|
38
40
|
};
|
|
39
41
|
/**
|
|
40
42
|
* Durable runs: `submit` starts one on the server and returns its stream id;
|
|
41
|
-
* `events` reads it from `after
|
|
42
|
-
*
|
|
43
|
+
* `events` reads it from `after`. The run lives on the server, so a reload
|
|
44
|
+
* re-attaches with `resume`. A stream that ends before the run's terminal
|
|
45
|
+
* fails with `Client.Disconnected`; pipe through `reconnect` to resume it.
|
|
43
46
|
*/
|
|
44
47
|
declare const durable: <I, S, ES, RS, EE, RE, EX = never, RX = never>(options: {
|
|
45
48
|
readonly submit: (input: I, state: S) => Effect.Effect<StreamId, ES, RS>;
|
|
46
49
|
readonly events: (streamId: StreamId, after: number) => Stream.Stream<Envelope, EE, RE>;
|
|
47
50
|
readonly stop?: (streamId: StreamId) => Effect.Effect<void, EX, RX>;
|
|
48
|
-
}) => Durable<I, S, ES | EE | EX, RS | RE | RX>;
|
|
51
|
+
}) => Durable<I, S, ES | EE | EX | Disconnected, RS | RE | RX>;
|
|
49
52
|
/**
|
|
50
53
|
* A durable session (a chat): one stream carries every run, and the server
|
|
51
54
|
* owns its history. `send` without a stream id starts a new session and
|
|
52
55
|
* returns its id (the app's own chat id, branded); with one, it adds a run to
|
|
53
|
-
* that session. `events` follows the whole session from `after
|
|
54
|
-
*
|
|
55
|
-
* not as a response to `send`.
|
|
56
|
+
* that session. `events` follows the whole session from `after`. The run
|
|
57
|
+
* arrives on that stream, not as a response to `send`.
|
|
56
58
|
*/
|
|
57
59
|
type Session<I, S, E, R> = {
|
|
58
60
|
readonly send: (streamId: Option.Option<StreamId>, input: I, state: S) => Effect.Effect<StreamId, E, R>;
|
|
59
61
|
readonly events: (streamId: StreamId, after: number) => Stream.Stream<Envelope, E, R>;
|
|
60
62
|
readonly stop?: (streamId: StreamId) => Effect.Effect<void, E, R>;
|
|
61
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* A session stream never ends on its own, so one that completes fails with
|
|
66
|
+
* `Client.Disconnected`; pipe through `reconnect` to resume it.
|
|
67
|
+
*/
|
|
62
68
|
declare const session: <I, S, ES, RS, EE, RE, EX = never, RX = never>(options: {
|
|
63
69
|
readonly send: (streamId: Option.Option<StreamId>, input: I, state: S) => Effect.Effect<StreamId, ES, RS>;
|
|
64
70
|
readonly events: (streamId: StreamId, after: number) => Stream.Stream<Envelope, EE, RE>;
|
|
65
71
|
readonly stop?: (streamId: StreamId) => Effect.Effect<void, EX, RX>;
|
|
66
|
-
}) => Session<I, S, ES | EE | EX, RS | RE | RX>;
|
|
72
|
+
}) => Session<I, S, ES | EE | EX | Disconnected, RS | RE | RX>;
|
|
67
73
|
/** What the HTTP helpers can fail with. */
|
|
68
74
|
type HttpError = ClientError | Disconnected;
|
|
69
75
|
type HttpOptions = {
|
|
@@ -71,13 +77,11 @@ type HttpOptions = {
|
|
|
71
77
|
readonly events: (streamId: StreamId, after: number) => HttpClientRequest.HttpClientRequest;
|
|
72
78
|
/** The request that stops a stream's active run. */
|
|
73
79
|
readonly stop?: (streamId: StreamId) => HttpClientRequest.HttpClientRequest;
|
|
74
|
-
/** When to reconnect a dropped event stream (it resumes from the last `seq`). */
|
|
75
|
-
readonly reconnect: Schedule.Schedule<unknown, HttpError>;
|
|
76
80
|
};
|
|
77
81
|
/**
|
|
78
82
|
* `durable` over HTTP. `submit` starts a run and returns its stream id (the
|
|
79
83
|
* app decodes its own response, e.g. with `HttpClientResponse.schemaJson`);
|
|
80
|
-
* `events` is read as SSE
|
|
84
|
+
* `events` is read as SSE. Pipe through `reconnect` to survive drops.
|
|
81
85
|
*/
|
|
82
86
|
declare const durableHttp: <I, S, ES = never, RS = never>(options: HttpOptions & {
|
|
83
87
|
readonly submit: (input: I, state: S) => Effect.Effect<StreamId, ES, RS>;
|
|
@@ -85,12 +89,49 @@ declare const durableHttp: <I, S, ES = never, RS = never>(options: HttpOptions &
|
|
|
85
89
|
/**
|
|
86
90
|
* `session` over HTTP. `send` adds an input and returns the session's stream
|
|
87
91
|
* id (without one, the server starts a session); `events` follows the whole
|
|
88
|
-
* session as SSE.
|
|
89
|
-
* `reconnect` usually retries forever (e.g. `Schedule.spaced("1 second")`).
|
|
92
|
+
* session as SSE. Pipe through `reconnect` to survive drops.
|
|
90
93
|
*/
|
|
91
94
|
declare const sessionHttp: <I, S, ES = never, RS = never>(options: HttpOptions & {
|
|
92
95
|
readonly send: (streamId: Option.Option<StreamId>, input: I, state: S) => Effect.Effect<StreamId, ES, RS>;
|
|
93
96
|
}) => Session<I, S, HttpError | ES, HttpClient.HttpClient | RS>;
|
|
97
|
+
type ReconnectOptions<E> = {
|
|
98
|
+
/**
|
|
99
|
+
* When to retry. Default: exponential from 500 ms, capped at 10 s, jittered.
|
|
100
|
+
* Cap your own exponential: the schedule resets only when a frame arrives,
|
|
101
|
+
* so an idle session's backoff otherwise grows without bound.
|
|
102
|
+
*/
|
|
103
|
+
readonly schedule?: Schedule.Schedule<unknown, E | Disconnected>;
|
|
104
|
+
/** More errors to treat as transient, on top of drops, timeouts, 408 / 429 / 5xx and SSE `retry:`. */
|
|
105
|
+
readonly while?: Predicate.Predicate<E>;
|
|
106
|
+
/** Max attempts in a row; the count resets once a frame arrives. Default: unlimited. */
|
|
107
|
+
readonly times?: number;
|
|
108
|
+
};
|
|
109
|
+
/** A transport `reconnect` applies to: one with a position to resume from. */
|
|
110
|
+
type Resumable = Session<any, any, any, any> | Durable<any, any, any, any>;
|
|
111
|
+
/** What a transport can fail with. */
|
|
112
|
+
type ErrorOf<T extends Resumable> = T extends Session<any, any, infer E, any> ? E : T extends Durable<any, any, infer E, any> ? E : never;
|
|
113
|
+
type WithDisconnected<F> = F extends ((...args: infer A) => Stream.Stream<infer X, infer E, infer R>) ? (...args: A) => Stream.Stream<X, E | Disconnected, R> : F;
|
|
114
|
+
/** `T` with `Client.Disconnected` added to its streams; `send` / `submit` keep their exact types. */
|
|
115
|
+
type Reconnected<T extends Resumable> = { readonly [K in keyof T]: WithDisconnected<T[K]>; };
|
|
116
|
+
/**
|
|
117
|
+
* Resume a dropped event stream from the last seq, as `HttpClient.retryTransient`
|
|
118
|
+
* does for requests. Retries drops (`Client.Disconnected`), timeouts, transport
|
|
119
|
+
* errors, 408 / 429 / 5xx and an SSE `retry:` line, waiting at least what the
|
|
120
|
+
* server asks for (`Retry-After`, `retry:`); anything else (404, 401, a
|
|
121
|
+
* malformed frame) ends the stream with that error. `while` adds the app's own
|
|
122
|
+
* transient errors (e.g. a dropped WebSocket).
|
|
123
|
+
*
|
|
124
|
+
* A session is followed forever; a durable run until its terminal. A durable
|
|
125
|
+
* `submit` is never retried, so an input is never sent twice. Not for `direct`
|
|
126
|
+
* / `local`: there the run is the request, with no position to resume from.
|
|
127
|
+
*
|
|
128
|
+
* const transport = Transport.reconnect(Transport.sessionHttp({ send, events, stop }))
|
|
129
|
+
* const ws = pipe(Transport.session({ send, events }), Transport.reconnect({ while: isDropped }))
|
|
130
|
+
*/
|
|
131
|
+
declare const reconnect: {
|
|
132
|
+
<E>(options?: ReconnectOptions<E>): <T extends Resumable>(self: T) => Reconnected<T>;
|
|
133
|
+
<T extends Resumable>(self: T, options?: ReconnectOptions<ErrorOf<T>>): Reconnected<T>;
|
|
134
|
+
};
|
|
94
135
|
//#endregion
|
|
95
|
-
export { Durable, HttpError, Session, Transport, direct, durable, durableHttp, local, session, sessionHttp, Transport_d_exports as t };
|
|
136
|
+
export { Durable, ErrorOf, HttpError, ReconnectOptions, Reconnected, Resumable, Session, Transport, direct, durable, durableHttp, local, reconnect, session, sessionHttp, Transport_d_exports as t };
|
|
96
137
|
//# sourceMappingURL=Transport.d.mts.map
|
package/dist/Transport.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Transport.d.mts","names":[],"sources":["../src/Transport.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"Transport.d.mts","names":[],"sources":["../src/Transport.ts"],"mappings":"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"}
|