@xandreed/core 0.3.0 → 0.8.0-next.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.
Files changed (164) hide show
  1. package/README.md +1 -1
  2. package/dist/conformance.entity.d.ts +15 -0
  3. package/dist/conformance.entity.js +7 -0
  4. package/dist/decision-record.entity.d.ts +31 -0
  5. package/dist/decision-record.entity.functions.d.ts +6 -0
  6. package/dist/decision-record.entity.functions.js +3 -0
  7. package/dist/decision-record.entity.js +24 -0
  8. package/dist/domain/agent-failure.entity.d.ts +6 -6
  9. package/dist/domain/agent-failure.entity.js +12 -1
  10. package/dist/domain/failure.entity.d.ts +2 -2
  11. package/dist/domain/loop-event.entity.d.ts +2 -0
  12. package/dist/domain/message.entity.d.ts +143 -156
  13. package/dist/domain/message.entity.js +6 -6
  14. package/dist/domain/model-call-policy.entity.d.ts +3 -3
  15. package/dist/domain/model-call-policy.entity.js +1 -1
  16. package/dist/domain/model-catalog.entity.d.ts +4 -4
  17. package/dist/domain/model-catalog.entity.js +1 -1
  18. package/dist/domain/model-selection.entity.d.ts +6 -13
  19. package/dist/domain/prompt-provenance.entity.d.ts +6 -6
  20. package/dist/domain/prompt-provenance.entity.js +4 -4
  21. package/dist/domain/token-usage.entity.d.ts +4 -4
  22. package/dist/harness/capability.entity.d.ts +88 -41
  23. package/dist/harness/capability.entity.functions.d.ts +30 -1
  24. package/dist/harness/capability.entity.functions.js +55 -2
  25. package/dist/harness/capability.entity.js +33 -12
  26. package/dist/harness/config.entity.d.ts +28 -28
  27. package/dist/harness/config.entity.js +4 -4
  28. package/dist/harness/plugin-stack.adapter.d.ts +24 -0
  29. package/dist/harness/plugin-stack.adapter.js +8 -0
  30. package/dist/harness/plugin-stack.adapter.test.d.ts +1 -0
  31. package/dist/harness/plugin.adapter.d.ts +15 -4
  32. package/dist/harness/plugin.adapter.js +18 -11
  33. package/dist/harness/plugin.adapter.test.d.ts +1 -0
  34. package/dist/harness/plugin.entity.d.ts +26 -10
  35. package/dist/harness/plugin.entity.js +1 -1
  36. package/dist/harness/session.entity.d.ts +20 -20
  37. package/dist/harness/session.entity.js +1 -1
  38. package/dist/harness/skill-files.d.ts +17 -0
  39. package/dist/harness/skill-files.js +25 -0
  40. package/dist/index.d.ts +48 -4
  41. package/dist/index.js +46 -4
  42. package/dist/loop/cacheKey.d.ts +3 -3
  43. package/dist/loop/cacheKey.js +4 -2
  44. package/dist/loop/mapping.d.ts +4 -5
  45. package/dist/loop/mapping.js +8 -16
  46. package/dist/loop/modelPolicy.d.ts +4 -4
  47. package/dist/loop/modelPolicy.js +2 -2
  48. package/dist/loop/promptProvenance.d.ts +2 -2
  49. package/dist/loop/promptProvenance.js +2 -2
  50. package/dist/loop/stepContext.d.ts +2 -2
  51. package/dist/loop/stepContext.js +2 -2
  52. package/dist/loop/streamFold.js +3 -3
  53. package/dist/loop/toolSchema.d.ts +17 -0
  54. package/dist/loop/toolSchema.js +25 -0
  55. package/dist/loop/toolSchema.test.d.ts +1 -0
  56. package/dist/mcp/bridge.d.ts +15 -15
  57. package/dist/mcp/bridge.js +14 -14
  58. package/dist/memory/memory-log.entity.d.ts +320 -0
  59. package/dist/memory/memory-log.entity.functions.d.ts +89 -0
  60. package/dist/memory/memory-log.entity.functions.js +180 -0
  61. package/dist/memory/memory-log.entity.js +75 -0
  62. package/dist/memory/memory-log.entity.test.d.ts +1 -0
  63. package/dist/memory/memory-session.d.ts +51 -0
  64. package/dist/memory/memory-session.js +97 -0
  65. package/dist/memory/memory-session.test.d.ts +1 -0
  66. package/dist/memory/memory.conformance.d.ts +32 -0
  67. package/dist/memory/memory.conformance.js +102 -0
  68. package/dist/ports/auth-store.port.d.ts +16 -18
  69. package/dist/ports/auth-store.port.js +13 -9
  70. package/dist/ports/capability.port.d.ts +131 -7
  71. package/dist/ports/capability.port.js +2 -2
  72. package/dist/ports/conversation-store.port.d.ts +54 -177
  73. package/dist/ports/conversation-store.port.js +9 -9
  74. package/dist/ports/file-system.port.d.ts +5 -7
  75. package/dist/ports/file-system.port.js +1 -1
  76. package/dist/ports/harness.port.d.ts +19 -13
  77. package/dist/ports/harness.port.js +10 -10
  78. package/dist/ports/mcp-client.port.d.ts +15 -47
  79. package/dist/ports/mcp-client.port.js +3 -3
  80. package/dist/ports/memory.port.d.ts +116 -0
  81. package/dist/ports/memory.port.js +7 -0
  82. package/dist/ports/model-catalog.port.d.ts +1 -1
  83. package/dist/ports/model-catalog.port.js +1 -1
  84. package/dist/ports/permission.port.d.ts +10 -0
  85. package/dist/ports/permission.port.js +4 -0
  86. package/dist/ports/run-context.port.d.ts +28 -0
  87. package/dist/ports/run-context.port.js +4 -0
  88. package/dist/ports/session-log.port.d.ts +25 -0
  89. package/dist/ports/session-log.port.js +4 -0
  90. package/dist/ports/sessions.port.d.ts +141 -0
  91. package/dist/ports/sessions.port.js +4 -0
  92. package/dist/ports/settings-store.port.d.ts +18 -56
  93. package/dist/ports/settings-store.port.js +3 -3
  94. package/dist/ports/shell.port.d.ts +9 -21
  95. package/dist/ports/shell.port.js +1 -1
  96. package/dist/ports/step-loop.port.d.ts +85 -0
  97. package/dist/ports/step-loop.port.js +4 -0
  98. package/dist/ports/tasks.port.d.ts +59 -0
  99. package/dist/ports/tasks.port.js +10 -0
  100. package/dist/ports/tool-registry.port.d.ts +53 -0
  101. package/dist/ports/tool-registry.port.js +4 -0
  102. package/dist/ports/turn-admission.port.d.ts +23 -0
  103. package/dist/ports/turn-admission.port.js +4 -0
  104. package/dist/ports/turn-events.port.d.ts +55 -0
  105. package/dist/ports/turn-events.port.js +7 -0
  106. package/dist/ports/turn-scope.port.d.ts +101 -0
  107. package/dist/ports/turn-scope.port.js +10 -0
  108. package/dist/ports/turn.port.d.ts +113 -0
  109. package/dist/ports/turn.port.js +0 -0
  110. package/dist/ports/utility-llm.port.d.ts +12 -31
  111. package/dist/ports/utility-llm.port.js +1 -1
  112. package/dist/session/chassis.js +7 -7
  113. package/dist/session/session-event.entity.d.ts +55 -0
  114. package/dist/session/session-event.entity.functions.d.ts +31 -0
  115. package/dist/session/session-event.entity.functions.js +42 -0
  116. package/dist/session/session-event.entity.js +37 -0
  117. package/dist/session/session-event.entity.test.d.ts +1 -0
  118. package/dist/session/session-log.conformance.d.ts +13 -0
  119. package/dist/session/session-log.conformance.js +167 -0
  120. package/dist/session/session-log.entity.d.ts +164 -0
  121. package/dist/session/session-log.entity.js +82 -0
  122. package/dist/session/session-log.memory.adapter.d.ts +7 -0
  123. package/dist/session/session-log.memory.adapter.js +82 -0
  124. package/dist/session/session-log.memory.adapter.test.d.ts +1 -0
  125. package/dist/session/sessions.entity.d.ts +201 -0
  126. package/dist/session/sessions.entity.js +98 -0
  127. package/dist/session/tasks.entity.d.ts +39 -0
  128. package/dist/session/tasks.entity.js +26 -0
  129. package/dist/session/turn-admission.open.adapter.d.ts +4 -0
  130. package/dist/session/turn-admission.open.adapter.js +3 -0
  131. package/dist/spec/SpecDoc.d.ts +30 -117
  132. package/dist/spec/SpecDoc.js +10 -10
  133. package/dist/spec/codec.js +1 -1
  134. package/dist/turn/model-request.entity.d.ts +99 -0
  135. package/dist/turn/model-request.entity.functions.d.ts +108 -0
  136. package/dist/turn/model-request.entity.functions.js +167 -0
  137. package/dist/turn/model-request.entity.js +41 -0
  138. package/dist/turn/model-request.entity.test.d.ts +1 -0
  139. package/dist/turn/prompt-sections.d.ts +10 -0
  140. package/dist/turn/prompt-sections.js +3 -0
  141. package/dist/turn/step-loop.conformance.d.ts +16 -0
  142. package/dist/turn/step-loop.conformance.js +164 -0
  143. package/dist/turn/testing.d.ts +30 -0
  144. package/dist/turn/turn-bus.d.ts +23 -0
  145. package/dist/turn/turn-bus.js +54 -0
  146. package/dist/turn/turn-bus.test.d.ts +1 -0
  147. package/dist/turn/turn-event.entity.d.ts +263 -0
  148. package/dist/turn/turn-event.entity.functions.d.ts +35 -0
  149. package/dist/turn/turn-event.entity.functions.js +21 -0
  150. package/dist/turn/turn-event.entity.js +101 -0
  151. package/dist/turn/turn-lifecycle.d.ts +32 -0
  152. package/dist/turn/turn-lifecycle.js +48 -0
  153. package/dist/turn/turn-run.d.ts +26 -0
  154. package/dist/turn/turn-run.js +105 -0
  155. package/dist/turn/turn.adapter.d.ts +21 -0
  156. package/dist/turn/turn.adapter.js +126 -0
  157. package/dist/turn/turn.adapter.test.d.ts +1 -0
  158. package/dist/turn/turn.conformance.d.ts +20 -0
  159. package/dist/turn/turn.conformance.js +74 -0
  160. package/dist/turn/user-message.entity.d.ts +14 -0
  161. package/dist/turn/user-message.entity.js +10 -0
  162. package/dist/util/json.d.ts +2 -2
  163. package/dist/util/json.js +10 -10
  164. package/package.json +2 -3
@@ -0,0 +1,42 @@
1
+ import { Effect, Option, Schema } from "effect";
2
+ import { canonicalJson } from "../memory/memory-log.entity.functions.js";
3
+ import { LogEntry } from "../memory/memory-log.entity.js";
4
+ import { TransientTurnEvents, TurnEvent } from "../turn/turn-event.entity.js";
5
+ import { MemoryKindOf, TurnEndedData, TurnStartedData } from "./session-event.entity.js";
6
+ const encodeEntry = Schema.encodeEffect(LogEntry), decodeEntry = Schema.decodeUnknownEffect(LogEntry), encodeTurnEvent = Schema.encodeSync(TurnEvent), tagOfKind = new Map(Object.entries(MemoryKindOf).map(([tag, kind]) => [kind, tag])), canonical = (data) => JSON.parse(canonicalJson(data)), turnOf = (event) => Option.getOrElse(event.turn, () => 0);
7
+ export const draftOfEntry = (entry) => entry.body._tag === "TurnStarted" ? Effect.succeed(Option.none()) : encodeEntry(entry).pipe(Effect.map((encoded) => {
8
+ const { _tag, ...body } = encoded.body, kind = MemoryKindOf[_tag];
9
+ return Option.some({
10
+ kind,
11
+ turn: Option.some(entry.turn),
12
+ data: { entry: encoded.id, runId: encoded.runId, step: encoded.step, at: encoded.at, body }
13
+ });
14
+ })), draftsOfEntries = (entries) => Effect.forEach(entries, draftOfEntry).pipe(Effect.map((drafts) => drafts.flatMap(Option.toArray))), turnStartedDraft = (turn, data) => Schema.encodeEffect(TurnStartedData)(data).pipe(Effect.map((encoded) => ({ kind: "turn.started", turn: Option.some(turn), data: { ...encoded } }))), turnEndedDraft = (turn, data) => Schema.encodeEffect(TurnEndedData)(data).pipe(Effect.map((encoded) => ({ kind: "turn.ended", turn: Option.some(turn), data: { ...encoded } }))), turnStartedOf = (event) => Schema.decodeUnknownEffect(TurnStartedData)(canonical(event.data)), turnEndedOf = (event) => Schema.decodeUnknownEffect(TurnEndedData)(canonical(event.data)), entryOfEvent = (event) => {
15
+ if (event.kind === "turn.started")
16
+ return turnStartedOf(event).pipe(Effect.flatMap((started) => decodeEntry({
17
+ id: started.entry,
18
+ runId: started.runId,
19
+ turn: turnOf(event),
20
+ step: 0,
21
+ at: started.at,
22
+ body: { _tag: "TurnStarted", prompt: started.userMessage.text }
23
+ })), Effect.map(Option.some));
24
+ const tag = tagOfKind.get(event.kind);
25
+ if (tag === void 0)
26
+ return Effect.succeed(Option.none());
27
+ const data = canonical(event.data), body = typeof data.body === "object" && data.body !== null ? data.body : {};
28
+ return decodeEntry({ id: data.entry, runId: data.runId, turn: turnOf(event), step: data.step, at: data.at, body: { ...body, _tag: tag } }).pipe(Effect.map(Option.some));
29
+ }, entriesOfEvents = (events) => Effect.forEach(events, entryOfEvent).pipe(Effect.map((entries) => entries.flatMap(Option.toArray)));
30
+ const recordedElsewhere = new Set(["turn.started", "turn.ended", "skills.activated"]), withoutPayload = (data) => Object.fromEntries(Object.entries(data).filter(([key]) => key !== "result" && key !== "encoded" && key !== "input"));
31
+ export const draftOfTurnEvent = (turn, event) => {
32
+ if (TransientTurnEvents.includes(event._tag) || recordedElsewhere.has(event._tag))
33
+ return Option.none();
34
+ if (event._tag === "host")
35
+ return Option.some({ kind: event.name, turn: Option.some(turn), data: event.data });
36
+ const { _tag, ...data } = encodeTurnEvent(event);
37
+ if (_tag === "assistant.message") {
38
+ const { step, model, usage } = data;
39
+ return Option.some({ kind: "step.usage", turn: Option.some(turn), data: { step, model, usage } });
40
+ }
41
+ return Option.some({ kind: _tag, turn: Option.some(turn), data: _tag === "tool.completed" ? withoutPayload(data) : data });
42
+ };
@@ -0,0 +1,37 @@
1
+ import { Schema } from "effect";
2
+ import { EntryId } from "../memory/memory-log.entity.js";
3
+ import { UserMessage } from "../turn/user-message.entity.js";
4
+ import { JsonObject } from "./session-log.entity.js";
5
+ export const TurnStartedData = Schema.Struct({
6
+ runId: Schema.String,
7
+ key: Schema.String,
8
+ origin: Schema.Literals(["user", "inbox"]),
9
+ userMessage: UserMessage,
10
+ command: JsonObject,
11
+ claimed: Schema.Array(Schema.String),
12
+ entry: EntryId,
13
+ at: Schema.Number
14
+ }), TurnEndReason = Schema.Literals(["completed", "partial", "failed", "cancelled", "interrupted"]), TurnEndedData = Schema.Struct({
15
+ reason: TurnEndReason,
16
+ failure: Schema.OptionFromNullOr(Schema.Struct({ code: Schema.String, message: Schema.String }))
17
+ }), MemoryKindOf = {
18
+ SystemPrepared: "memory.system",
19
+ TurnContext: "memory.section",
20
+ StepContext: "memory.step",
21
+ Message: "memory.message",
22
+ ToolResult: "memory.tool-result",
23
+ ToolDigest: "memory.digest",
24
+ ToolsActivated: "memory.skills",
25
+ Compaction: "memory.compaction",
26
+ TurnEnded: "turn.reply"
27
+ }, MEMORY_KINDS = ["turn.started", ...Object.values(MemoryKindOf)], TURN_EVENT_KINDS = [
28
+ "step.started",
29
+ "step.ended",
30
+ "step.usage",
31
+ "tool.started",
32
+ "tool.completed",
33
+ "completion.evaluated",
34
+ "context.built",
35
+ "decision.recorded",
36
+ "request.prepared"
37
+ ], SESSION_KINDS = ["turn.ended", "inbox.queued", "inbox.dropped"], RESERVED_KINDS = [...MEMORY_KINDS, ...TURN_EVENT_KINDS, ...SESSION_KINDS];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { Context } from "effect";
2
+ import type { ConformanceCheck } from "../conformance.entity.js";
3
+ import type { SessionLog } from "../ports/session-log.port.js";
4
+ type Log = Context.Service.Shape<typeof SessionLog>;
5
+ /**
6
+ * The SessionLog contract, as checks any backend must pass: dense sequence
7
+ * numbers, one revision per commit, compare-and-swap, the `notAfter` clock
8
+ * check, JSON kept by value, reads by position and kind, listing order,
9
+ * children and their removal. Every check uses fresh ids and a fresh owner,
10
+ * so the kit is safe on a shared database.
11
+ */
12
+ export declare const sessionLogConformance: (log: Log) => ReadonlyArray<ConformanceCheck>;
13
+ export {};
@@ -0,0 +1,167 @@
1
+ import { Effect, Option } from "effect";
2
+ import { ConformanceFailure } from "../conformance.entity.js";
3
+ import { ConversationId } from "../domain/message.entity.js";
4
+ import { canonicalJson } from "../memory/memory-log.entity.functions.js";
5
+ const fail = (check, message) => Effect.fail(new ConformanceFailure({ check, message })), expect = (check, holds, message) => holds ? Effect.void : fail(check, message), freshId = Effect.sync(() => ConversationId.make(crypto.randomUUID())), headerOf = (id, owner, parent = Option.none()) => ({
6
+ id,
7
+ owner,
8
+ origin: "user",
9
+ createdAt: 1767225600000,
10
+ meta: { title: "conformance", nested: { list: [1, "two", null] } },
11
+ parent
12
+ }), draft = (kind, data, turn = Option.none()) => ({ kind, turn, data }), noEvents = { notAfter: Option.none(), state: Option.none() }, rich = { zeta: 1, alpha: { b: [!0, !1, { y: "\xFC", x: 0.5 }], a: "text \u2713" }, empty: {}, list: [] };
13
+ export const sessionLogConformance = (log) => {
14
+ const check = (name, id, body) => ({
15
+ name,
16
+ run: freshId.pipe(Effect.flatMap((owner) => body(`owner-${owner}`)), Effect.catch((error) => error instanceof ConformanceFailure ? Effect.fail(error) : fail(id, `unexpected ${typeof error === "object" && error !== null && "_tag" in error ? String(error._tag) : "error"}: ${JSON.stringify(error)}`)))
17
+ });
18
+ return [
19
+ check("a new session is empty: seq 0, revision 0, the header as given", "create", (owner) => Effect.gen(function* () {
20
+ const id = yield* freshId, created = yield* log.create(headerOf(id, owner)), head = yield* log.head(id);
21
+ yield* expect("create", created.seq === 0 && created.revision === 0 && head.seq === 0 && head.revision === 0, `seq ${head.seq}, revision ${head.revision}`);
22
+ yield* expect("create", head.header.owner === owner && head.header.origin === "user" && Option.isNone(head.header.parent), "the header changed");
23
+ yield* expect("create", canonicalJson(head.header.meta) === canonicalJson(headerOf(id, owner).meta), "meta changed");
24
+ yield* expect("create", canonicalJson(head.state) === "{}", "a new session has state");
25
+ const again = yield* Effect.flip(log.create(headerOf(id, owner)));
26
+ yield* expect("create", again._tag === "SessionExists", `creating twice gave ${again._tag}`);
27
+ })),
28
+ check("commits number events densely from 1, in order, and bump the revision once", "append", (owner) => Effect.gen(function* () {
29
+ const id = yield* freshId;
30
+ yield* log.create(headerOf(id, owner));
31
+ const first = yield* log.commit(id, { expect: 0, ...noEvents, events: [draft("a", { n: 1 }, Option.some(1)), draft("b", { n: 2 }, Option.some(1))] }), second = yield* log.commit(id, { expect: 1, ...noEvents, events: [draft("c", { n: 3 })] });
32
+ yield* expect("append", first.revision === 1 && second.revision === 2 && second.seq === 3, `revisions ${first.revision}, ${second.revision}; seq ${second.seq}`);
33
+ yield* expect("append", first.events.map((event) => event.seq).join() === "1,2" && second.events.map((event) => event.seq).join() === "3", "sequence numbers are not dense");
34
+ const stored = yield* log.read(id, { after: 0, limit: Option.none(), kinds: [] });
35
+ yield* expect("append", stored.map((event) => `${event.seq}:${event.kind}:${Option.getOrElse(event.turn, () => 0)}`).join() === "1:a:1,2:b:1,3:c:0", "the log differs");
36
+ yield* expect("append", stored.every((event) => event.session === id && event.at === (event.seq < 3 ? first.at : second.at)), "event times are not the commit's time");
37
+ const head = yield* log.head(id);
38
+ yield* expect("append", head.seq === 3 && head.revision === 2 && head.updatedAt === second.at, `head seq ${head.seq}, revision ${head.revision}`);
39
+ })),
40
+ check("a state-only commit bumps the revision and appends nothing", "state", (owner) => Effect.gen(function* () {
41
+ const id = yield* freshId;
42
+ yield* log.create(headerOf(id, owner));
43
+ const committed = yield* log.commit(id, { expect: 0, notAfter: Option.none(), events: [], state: Option.some({ open: { turn: 1 } }) }), head = yield* log.head(id);
44
+ yield* expect("state", committed.revision === 1 && committed.events.length === 0 && head.seq === 0 && head.revision === 1, `revision ${head.revision}, seq ${head.seq}`);
45
+ yield* expect("state", canonicalJson(head.state) === canonicalJson({ open: { turn: 1 } }), "the state was not replaced");
46
+ yield* log.commit(id, { expect: 1, ...noEvents, events: [draft("a", {})] });
47
+ yield* expect("state", canonicalJson((yield* log.head(id)).state) === canonicalJson({ open: { turn: 1 } }), "a commit without state changed it");
48
+ })),
49
+ check("a commit against a stale revision is refused and writes nothing", "conflict", (owner) => Effect.gen(function* () {
50
+ const id = yield* freshId;
51
+ yield* log.create(headerOf(id, owner));
52
+ yield* log.commit(id, { expect: 0, ...noEvents, events: [draft("a", {})] });
53
+ const refused = yield* Effect.flip(log.commit(id, { expect: 0, notAfter: Option.none(), events: [draft("b", {})], state: Option.some({ lost: !0 }) }));
54
+ yield* expect("conflict", refused._tag === "RevisionConflict" && refused.expected === 0 && refused.actual === 1, `refused with ${JSON.stringify(refused)}`);
55
+ const head = yield* log.head(id);
56
+ yield* expect("conflict", head.seq === 1 && head.revision === 1 && canonicalJson(head.state) === "{}", "a refused commit wrote something");
57
+ })),
58
+ check("a commit after its notAfter is refused by the storage clock and writes nothing", "expiry", (owner) => Effect.gen(function* () {
59
+ const id = yield* freshId;
60
+ yield* log.create(headerOf(id, owner));
61
+ const head = yield* log.head(id), refused = yield* Effect.flip(log.commit(id, { expect: 0, notAfter: Option.some(head.now - 60000), events: [draft("late", {})], state: Option.none() }));
62
+ yield* expect("expiry", refused._tag === "LeaseExpired", `refused with ${refused._tag}`);
63
+ const inTime = yield* log.commit(id, { expect: 0, notAfter: Option.some(head.now + 600000), events: [draft("on-time", {})], state: Option.none() });
64
+ yield* expect("expiry", inTime.revision === 1 && (yield* log.head(id)).seq === 1, "a commit in time was refused or the late one was written");
65
+ })),
66
+ check("of concurrent commits against one revision, exactly one wins", "race", (owner) => Effect.gen(function* () {
67
+ const id = yield* freshId;
68
+ yield* log.create(headerOf(id, owner));
69
+ const attempts = yield* Effect.forEach([0, 1, 2, 3, 4, 5, 6, 7], (n) => Effect.result(log.commit(id, { expect: 0, notAfter: Option.none(), events: [draft("attempt", { n })], state: Option.some({ winner: n }) })), { concurrency: "unbounded" }), won = attempts.filter((attempt) => attempt._tag === "Success"), refused = attempts.filter((attempt) => attempt._tag === "Failure" && attempt.failure._tag === "RevisionConflict");
70
+ yield* expect("race", won.length === 1 && refused.length === 7, `${won.length} won, ${refused.length} refused`);
71
+ const head = yield* log.head(id), events = yield* log.read(id, { after: 0, limit: Option.none(), kinds: [] });
72
+ yield* expect("race", head.revision === 1 && events.length === 1 && canonicalJson(head.state) === canonicalJson({ winner: events[0]?.data.n }), "the winner's state and event disagree");
73
+ })),
74
+ check("event data and state come back equal, whatever key order the store keeps", "json", (owner) => Effect.gen(function* () {
75
+ const id = yield* freshId;
76
+ yield* log.create(headerOf(id, owner));
77
+ yield* log.commit(id, { expect: 0, notAfter: Option.none(), events: [draft("rich", rich)], state: Option.some(rich) });
78
+ const [event] = yield* log.read(id, { after: 0, limit: Option.none(), kinds: [] });
79
+ yield* expect("json", event !== void 0 && canonicalJson(event.data) === canonicalJson(rich), `data came back as ${JSON.stringify(event?.data)}`);
80
+ yield* expect("json", canonicalJson((yield* log.head(id)).state) === canonicalJson(rich), "the state came back different");
81
+ })),
82
+ check("input and returned JSON are detached from stored heads and events", "json-isolation", (owner) => Effect.gen(function* () {
83
+ const id = yield* freshId, header = headerOf(id, owner), expectedMeta = canonicalJson(header.meta), created = yield* log.create(header), change = (json, by) => {
84
+ Reflect.set(json, "changed", by);
85
+ if (typeof json.nested === "object" && json.nested !== null) {
86
+ Reflect.set(json.nested, "text", by);
87
+ if ("list" in json.nested && typeof json.nested.list === "object" && json.nested.list !== null)
88
+ Reflect.set(json.nested.list, "0", by);
89
+ }
90
+ if (typeof json.list === "object" && json.list !== null)
91
+ Reflect.set(json.list, "0", by);
92
+ };
93
+ change(header.meta, "input");
94
+ change(created.header.meta, "create");
95
+ Reflect.set(created.state, "changed", "create");
96
+ yield* expect("json-isolation", canonicalJson((yield* log.head(id)).state) === "{}", "a returned create state changed the new session");
97
+ const value = { nested: { text: "original" }, list: ["original"] }, expected = canonicalJson(value), committed = yield* log.commit(id, { expect: 0, notAfter: Option.none(), events: [draft("fact", value)], state: Option.some(value) });
98
+ value.nested.text = "input";
99
+ value.list[0] = "input";
100
+ const head = yield* log.head(id), listed = yield* log.list({ owner, limit: 10, before: Option.none(), parent: Option.none() }), events = yield* log.read(id, { after: 0, limit: Option.none(), kinds: [] });
101
+ change(head.header.meta, "output");
102
+ change(head.state, "output");
103
+ listed.forEach((entry) => {
104
+ change(entry.header.meta, "output");
105
+ change(entry.state, "output");
106
+ });
107
+ committed.events.forEach((event) => change(event.data, "output"));
108
+ events.forEach((event) => change(event.data, "output"));
109
+ const again = yield* log.head(id), reread = yield* log.read(id, { after: 0, limit: Option.none(), kinds: [] });
110
+ yield* expect("json-isolation", again.revision === 1 && again.seq === 1, "mutating a returned value moved the head");
111
+ yield* expect("json-isolation", canonicalJson(again.header.meta) === expectedMeta, "input, create, head or list metadata changed the stored header");
112
+ yield* expect("json-isolation", canonicalJson(again.state) === expected, "input, head or list state changed the stored state");
113
+ yield* expect("json-isolation", reread.length === 1 && canonicalJson(reread[0].data) === expected, "input, commit or read data changed the stored event");
114
+ })),
115
+ check("reads return events after a position, ascending, filtered by kind and limited", "read", (owner) => Effect.gen(function* () {
116
+ const id = yield* freshId;
117
+ yield* log.create(headerOf(id, owner));
118
+ yield* log.commit(id, { expect: 0, ...noEvents, events: ["a", "b", "a", "c", "a"].map((kind, n) => draft(kind, { n })) });
119
+ const seqs = (events) => events.map((event) => event.seq).join();
120
+ yield* expect("read", seqs(yield* log.read(id, { after: 2, limit: Option.none(), kinds: [] })) === "3,4,5", "after is not exclusive");
121
+ yield* expect("read", seqs(yield* log.read(id, { after: 0, limit: Option.none(), kinds: ["a"] })) === "1,3,5", "kinds do not filter");
122
+ yield* expect("read", seqs(yield* log.read(id, { after: 1, limit: Option.some(2), kinds: ["a", "c"] })) === "3,4", "limit does not apply after the filter");
123
+ yield* expect("read", (yield* log.read(id, { after: 5, limit: Option.none(), kinds: [] })).length === 0, "reading past the end returned events");
124
+ })),
125
+ check("a listing holds the owner's top-level sessions, newest first, before the cursor", "list", (owner) => Effect.gen(function* () {
126
+ const [first, second, third] = [yield* freshId, yield* freshId, yield* freshId];
127
+ yield* log.create(headerOf(first, owner));
128
+ yield* log.create(headerOf(second, owner));
129
+ yield* log.create(headerOf(third, owner));
130
+ yield* log.create(headerOf(yield* freshId, `${owner}-other`));
131
+ yield* log.create(headerOf(yield* freshId, owner, Option.some({ id: first, through: 0, turnAtFork: 0 })));
132
+ yield* Effect.sleep("5 millis");
133
+ yield* log.commit(second, { expect: 0, ...noEvents, events: [draft("touch", {})] });
134
+ const listed = yield* log.list({ owner, limit: 10, before: Option.none(), parent: Option.none() });
135
+ yield* expect("list", listed.length === 3 && listed[0]?.header.id === second, `listed ${listed.map((head) => head.header.id).join()}`);
136
+ yield* expect("list", listed.every((head, index) => index === 0 || (listed[index - 1].updatedAt > head.updatedAt || listed[index - 1].updatedAt === head.updatedAt && listed[index - 1].header.id > head.header.id)), "the listing is not newest first, then by id");
137
+ const cursor = listed[0], rest = yield* log.list({ owner, limit: 10, before: Option.some({ updatedAt: cursor.updatedAt, id: cursor.header.id }), parent: Option.none() });
138
+ yield* expect("list", rest.length === 2 && rest.every((head) => head.header.id !== second), "the cursor is not exclusive");
139
+ yield* expect("list", (yield* log.list({ owner, limit: 1, before: Option.none(), parent: Option.none() })).length === 1, "the limit is ignored");
140
+ const children = yield* log.list({ owner, limit: 10, before: Option.none(), parent: Option.some(first) });
141
+ yield* expect("list", children.length === 1 && Option.exists(children[0].header.parent, (parent) => parent.id === first), "children are not listed under their parent");
142
+ })),
143
+ check("a child needs its parent, and removing a session removes its events and children", "lineage", (owner) => Effect.gen(function* () {
144
+ const [parent, child, grandchild, orphan] = [yield* freshId, yield* freshId, yield* freshId, yield* freshId], lost = yield* Effect.flip(log.create(headerOf(orphan, owner, Option.some({ id: yield* freshId, through: 0, turnAtFork: 0 }))));
145
+ yield* expect("lineage", lost._tag === "SessionMissing", `a child of a missing parent gave ${lost._tag}`);
146
+ yield* log.create(headerOf(parent, owner));
147
+ yield* log.commit(parent, { expect: 0, ...noEvents, events: [draft("a", {})] });
148
+ const forked = yield* log.create(headerOf(child, owner, Option.some({ id: parent, through: 1, turnAtFork: 1 })));
149
+ yield* expect("lineage", Option.exists(forked.header.parent, (lineage) => lineage.id === parent && lineage.through === 1 && lineage.turnAtFork === 1), "the lineage changed");
150
+ yield* log.create(headerOf(grandchild, owner, Option.some({ id: child, through: 0, turnAtFork: 1 })));
151
+ yield* log.remove(parent);
152
+ const gone = yield* Effect.forEach([parent, child, grandchild], (id) => Effect.flip(log.head(id)));
153
+ yield* expect("lineage", gone.every((error) => error._tag === "SessionMissing"), "a removed session or child is still there");
154
+ const read = yield* Effect.flip(log.read(parent, { after: 0, limit: Option.none(), kinds: [] }));
155
+ yield* expect("lineage", read._tag === "SessionMissing", "a removed session still reads");
156
+ yield* log.remove(parent);
157
+ })),
158
+ check("a missing session is SessionMissing to every operation", "missing", () => Effect.gen(function* () {
159
+ const id = yield* freshId, errors = [
160
+ yield* Effect.flip(log.head(id)),
161
+ yield* Effect.flip(log.read(id, { after: 0, limit: Option.none(), kinds: [] })),
162
+ yield* Effect.flip(log.commit(id, { expect: 0, ...noEvents, events: [draft("a", {})] }))
163
+ ];
164
+ yield* expect("missing", errors.every((error) => error._tag === "SessionMissing"), `got ${errors.map((error) => error._tag).join()}`);
165
+ }))
166
+ ];
167
+ };
@@ -0,0 +1,164 @@
1
+ import { Schema } from "effect";
2
+ /** A JSON object as stored: compared by value, key order is not kept. */
3
+ export declare const JsonObject: Schema.$Record<Schema.String, Schema.Unknown>;
4
+ export type JsonObject = typeof JsonObject.Type;
5
+ /** Where a forked session starts: the parent, the last parent event it inherits, and the parent's turns at that point. */
6
+ export declare const SessionLineage: Schema.Struct<{
7
+ readonly id: Schema.brand<Schema.String, "ConversationId">;
8
+ readonly through: Schema.Int;
9
+ readonly turnAtFork: Schema.Int;
10
+ }>;
11
+ export type SessionLineage = typeof SessionLineage.Type;
12
+ /** What a session is, fixed when it is created. */
13
+ export declare const SessionHeader: Schema.Struct<{
14
+ readonly id: Schema.brand<Schema.String, "ConversationId">;
15
+ /** Every read and write names the owner; another owner sees no session. */
16
+ readonly owner: Schema.Trimmed;
17
+ /** Who opened it: `user` for a conversation, `task` for a background task, or a host's own kind. */
18
+ readonly origin: Schema.Trimmed;
19
+ readonly createdAt: Schema.Number;
20
+ readonly meta: Schema.$Record<Schema.String, Schema.Unknown>;
21
+ readonly parent: Schema.OptionFromNullOr<Schema.Struct<{
22
+ readonly id: Schema.brand<Schema.String, "ConversationId">;
23
+ readonly through: Schema.Int;
24
+ readonly turnAtFork: Schema.Int;
25
+ }>>;
26
+ }>;
27
+ export type SessionHeader = typeof SessionHeader.Type;
28
+ /** A session's head: the header, the last sequence number, the revision and the plugin's state. */
29
+ export declare const SessionHead: Schema.Struct<{
30
+ readonly header: Schema.Struct<{
31
+ readonly id: Schema.brand<Schema.String, "ConversationId">;
32
+ /** Every read and write names the owner; another owner sees no session. */
33
+ readonly owner: Schema.Trimmed;
34
+ /** Who opened it: `user` for a conversation, `task` for a background task, or a host's own kind. */
35
+ readonly origin: Schema.Trimmed;
36
+ readonly createdAt: Schema.Number;
37
+ readonly meta: Schema.$Record<Schema.String, Schema.Unknown>;
38
+ readonly parent: Schema.OptionFromNullOr<Schema.Struct<{
39
+ readonly id: Schema.brand<Schema.String, "ConversationId">;
40
+ readonly through: Schema.Int;
41
+ readonly turnAtFork: Schema.Int;
42
+ }>>;
43
+ }>;
44
+ /** The last event's sequence number; 0 before any event. */
45
+ readonly seq: Schema.Int;
46
+ /** Bumped by exactly one on every commit, events or not. */
47
+ readonly revision: Schema.Int;
48
+ readonly state: Schema.$Record<Schema.String, Schema.Unknown>;
49
+ readonly updatedAt: Schema.Number;
50
+ /** The storage clock when the head was read. */
51
+ readonly now: Schema.Number;
52
+ }>;
53
+ export type SessionHead = typeof SessionHead.Type;
54
+ /** An event to append; the log assigns its sequence number and time. */
55
+ export declare const SessionDraft: Schema.Struct<{
56
+ readonly kind: Schema.Trimmed;
57
+ readonly turn: Schema.OptionFromNullOr<Schema.Int>;
58
+ readonly data: Schema.$Record<Schema.String, Schema.Unknown>;
59
+ }>;
60
+ export type SessionDraft = typeof SessionDraft.Type;
61
+ /** One stored event. */
62
+ export declare const SessionLogEvent: Schema.Struct<{
63
+ readonly session: Schema.brand<Schema.String, "ConversationId">;
64
+ /** Dense from 1 within the session. */
65
+ readonly seq: Schema.Int;
66
+ readonly turn: Schema.OptionFromNullOr<Schema.Int>;
67
+ readonly kind: Schema.Trimmed;
68
+ readonly at: Schema.Number;
69
+ readonly data: Schema.$Record<Schema.String, Schema.Unknown>;
70
+ }>;
71
+ export type SessionLogEvent = typeof SessionLogEvent.Type;
72
+ /**
73
+ * One atomic commit: accepted only while the head's revision is `expect`
74
+ * and the storage clock is at most `notAfter` (when given). `state`, when
75
+ * given, replaces the head's state; `events` may be empty.
76
+ */
77
+ export declare const SessionCommit: Schema.Struct<{
78
+ readonly expect: Schema.Int;
79
+ readonly notAfter: Schema.OptionFromNullOr<Schema.Number>;
80
+ readonly events: Schema.$Array<Schema.Struct<{
81
+ readonly kind: Schema.Trimmed;
82
+ readonly turn: Schema.OptionFromNullOr<Schema.Int>;
83
+ readonly data: Schema.$Record<Schema.String, Schema.Unknown>;
84
+ }>>;
85
+ readonly state: Schema.OptionFromNullOr<Schema.$Record<Schema.String, Schema.Unknown>>;
86
+ }>;
87
+ export type SessionCommit = typeof SessionCommit.Type;
88
+ /** What a commit stored: the new revision, the events as stored, and the storage time. */
89
+ export declare const SessionCommitted: Schema.Struct<{
90
+ readonly revision: Schema.Int;
91
+ readonly seq: Schema.Int;
92
+ readonly events: Schema.$Array<Schema.Struct<{
93
+ readonly session: Schema.brand<Schema.String, "ConversationId">;
94
+ /** Dense from 1 within the session. */
95
+ readonly seq: Schema.Int;
96
+ readonly turn: Schema.OptionFromNullOr<Schema.Int>;
97
+ readonly kind: Schema.Trimmed;
98
+ readonly at: Schema.Number;
99
+ readonly data: Schema.$Record<Schema.String, Schema.Unknown>;
100
+ }>>;
101
+ readonly at: Schema.Number;
102
+ }>;
103
+ export type SessionCommitted = typeof SessionCommitted.Type;
104
+ /** Events after `after`, ascending; only the given kinds when any are named. */
105
+ export declare const SessionReadQuery: Schema.Struct<{
106
+ readonly after: Schema.Int;
107
+ readonly limit: Schema.OptionFromNullOr<Schema.Int>;
108
+ readonly kinds: Schema.$Array<Schema.Trimmed>;
109
+ }>;
110
+ export type SessionReadQuery = typeof SessionReadQuery.Type;
111
+ /** A position in a listing: strictly older sessions come after it. */
112
+ export declare const SessionListCursor: Schema.Struct<{
113
+ readonly updatedAt: Schema.Number;
114
+ readonly id: Schema.brand<Schema.String, "ConversationId">;
115
+ }>;
116
+ export type SessionListCursor = typeof SessionListCursor.Type;
117
+ /**
118
+ * An owner's sessions, most recently updated first (then by id, descending).
119
+ * With `parent`, that session's children; without, only top-level sessions.
120
+ */
121
+ export declare const SessionListQuery: Schema.Struct<{
122
+ readonly owner: Schema.Trimmed;
123
+ readonly limit: Schema.Int;
124
+ readonly before: Schema.OptionFromNullOr<Schema.Struct<{
125
+ readonly updatedAt: Schema.Number;
126
+ readonly id: Schema.brand<Schema.String, "ConversationId">;
127
+ }>>;
128
+ readonly parent: Schema.OptionFromNullOr<Schema.brand<Schema.String, "ConversationId">>;
129
+ }>;
130
+ export type SessionListQuery = typeof SessionListQuery.Type;
131
+ declare const SessionMissing_base: Schema.Class<SessionMissing, Schema.TaggedStruct<"SessionMissing", {
132
+ readonly session: Schema.brand<Schema.String, "ConversationId">;
133
+ }>, import("effect/Cause").YieldableError>;
134
+ export declare class SessionMissing extends SessionMissing_base {
135
+ }
136
+ declare const SessionExists_base: Schema.Class<SessionExists, Schema.TaggedStruct<"SessionExists", {
137
+ readonly session: Schema.brand<Schema.String, "ConversationId">;
138
+ }>, import("effect/Cause").YieldableError>;
139
+ export declare class SessionExists extends SessionExists_base {
140
+ }
141
+ declare const RevisionConflict_base: Schema.Class<RevisionConflict, Schema.TaggedStruct<"RevisionConflict", {
142
+ readonly session: Schema.brand<Schema.String, "ConversationId">;
143
+ readonly expected: Schema.Int;
144
+ readonly actual: Schema.Int;
145
+ }>, import("effect/Cause").YieldableError>;
146
+ /** The head moved on since the writer read it; nothing was written. */
147
+ export declare class RevisionConflict extends RevisionConflict_base {
148
+ }
149
+ declare const LeaseExpired_base: Schema.Class<LeaseExpired, Schema.TaggedStruct<"LeaseExpired", {
150
+ readonly session: Schema.brand<Schema.String, "ConversationId">;
151
+ readonly notAfter: Schema.Number;
152
+ readonly now: Schema.Number;
153
+ }>, import("effect/Cause").YieldableError>;
154
+ /** The storage clock passed the commit's `notAfter`; nothing was written. */
155
+ export declare class LeaseExpired extends LeaseExpired_base {
156
+ }
157
+ declare const SessionLogError_base: Schema.Class<SessionLogError, Schema.TaggedStruct<"SessionLogError", {
158
+ readonly code: Schema.String;
159
+ readonly message: Schema.String;
160
+ }>, import("effect/Cause").YieldableError>;
161
+ /** The store itself failed (unreachable, undecodable). */
162
+ export declare class SessionLogError extends SessionLogError_base {
163
+ }
164
+ export {};
@@ -0,0 +1,82 @@
1
+ import { Schema } from "effect";
2
+ import { ConversationId } from "../domain/message.entity.js";
3
+ const Name = Schema.Trimmed.check(Schema.isNonEmpty());
4
+ export const JsonObject = Schema.Record(Schema.String, Schema.Unknown), SessionLineage = Schema.Struct({
5
+ id: ConversationId,
6
+ through: Schema.Int,
7
+ turnAtFork: Schema.Int
8
+ }), SessionHeader = Schema.Struct({
9
+ id: ConversationId,
10
+ owner: Name,
11
+ origin: Name,
12
+ createdAt: Schema.Number,
13
+ meta: JsonObject,
14
+ parent: Schema.OptionFromNullOr(SessionLineage)
15
+ }), SessionHead = Schema.Struct({
16
+ header: SessionHeader,
17
+ seq: Schema.Int,
18
+ revision: Schema.Int,
19
+ state: JsonObject,
20
+ updatedAt: Schema.Number,
21
+ now: Schema.Number
22
+ }), SessionDraft = Schema.Struct({
23
+ kind: Name,
24
+ turn: Schema.OptionFromNullOr(Schema.Int),
25
+ data: JsonObject
26
+ }), SessionLogEvent = Schema.Struct({
27
+ session: ConversationId,
28
+ seq: Schema.Int,
29
+ turn: Schema.OptionFromNullOr(Schema.Int),
30
+ kind: Name,
31
+ at: Schema.Number,
32
+ data: JsonObject
33
+ }), SessionCommit = Schema.Struct({
34
+ expect: Schema.Int,
35
+ notAfter: Schema.OptionFromNullOr(Schema.Number),
36
+ events: Schema.Array(SessionDraft),
37
+ state: Schema.OptionFromNullOr(JsonObject)
38
+ }), SessionCommitted = Schema.Struct({
39
+ revision: Schema.Int,
40
+ seq: Schema.Int,
41
+ events: Schema.Array(SessionLogEvent),
42
+ at: Schema.Number
43
+ }), SessionReadQuery = Schema.Struct({
44
+ after: Schema.Int,
45
+ limit: Schema.OptionFromNullOr(Schema.Int),
46
+ kinds: Schema.Array(Name)
47
+ }), SessionListCursor = Schema.Struct({ updatedAt: Schema.Number, id: ConversationId }), SessionListQuery = Schema.Struct({
48
+ owner: Name,
49
+ limit: Schema.Int,
50
+ before: Schema.OptionFromNullOr(SessionListCursor),
51
+ parent: Schema.OptionFromNullOr(ConversationId)
52
+ });
53
+
54
+ export class SessionMissing extends Schema.TaggedError()("SessionMissing", {
55
+ session: ConversationId
56
+ }) {
57
+ }
58
+
59
+ export class SessionExists extends Schema.TaggedError()("SessionExists", {
60
+ session: ConversationId
61
+ }) {
62
+ }
63
+
64
+ export class RevisionConflict extends Schema.TaggedError()("RevisionConflict", {
65
+ session: ConversationId,
66
+ expected: Schema.Int,
67
+ actual: Schema.Int
68
+ }) {
69
+ }
70
+
71
+ export class LeaseExpired extends Schema.TaggedError()("LeaseExpired", {
72
+ session: ConversationId,
73
+ notAfter: Schema.Number,
74
+ now: Schema.Number
75
+ }) {
76
+ }
77
+
78
+ export class SessionLogError extends Schema.TaggedError()("SessionLogError", {
79
+ code: Schema.String,
80
+ message: Schema.String
81
+ }) {
82
+ }
@@ -0,0 +1,7 @@
1
+ import { Layer } from "effect";
2
+ import { SessionLog } from "../ports/session-log.port.js";
3
+ /**
4
+ * The session log held in memory, for tests and single-process hosts. Its
5
+ * clock is the Effect clock, so a TestClock drives leases.
6
+ */
7
+ export declare const SessionLogMemoryLive: Layer.Layer<SessionLog>;
@@ -0,0 +1,82 @@
1
+ import { Clock, Effect, Layer, Option, Ref } from "effect";
2
+ import { SessionLog } from "../ports/session-log.port.js";
3
+ import { LeaseExpired, RevisionConflict, SessionExists, SessionMissing } from "./session-log.entity.js";
4
+ const stored = (value) => JSON.parse(JSON.stringify(value)), copyHeader = (header) => ({
5
+ ...header,
6
+ meta: stored(header.meta),
7
+ parent: Option.map(header.parent, (parent) => ({ ...parent }))
8
+ }), copyHead = (head, now) => ({
9
+ ...head,
10
+ header: copyHeader(head.header),
11
+ state: stored(head.state),
12
+ now
13
+ }), copyEvent = (event) => ({
14
+ ...event,
15
+ turn: Option.map(event.turn, (turn) => turn),
16
+ data: stored(event.data)
17
+ }), withDescendants = (all, ids) => {
18
+ const next = new Set([...ids, ...[...all.entries()].flatMap(([id, session]) => Option.match(session.head.header.parent, { onNone: () => [], onSome: (parent) => ids.has(parent.id) ? [id] : [] }))]);
19
+ return next.size === ids.size ? ids : withDescendants(all, next);
20
+ }, olderThan = (cursor) => (head) => head.updatedAt < cursor.updatedAt || head.updatedAt === cursor.updatedAt && head.header.id < cursor.id;
21
+ export const SessionLogMemoryLive = Layer.effect(SessionLog, Effect.gen(function* () {
22
+ const store = yield* Ref.make(new Map), now = Clock.currentTimeMillis, missing = (id) => new SessionMissing({ session: id });
23
+ return SessionLog.of({
24
+ create: (header) => now.pipe(Effect.flatMap((at) => Ref.modify(store, (all) => {
25
+ if (all.has(header.id))
26
+ return [Effect.fail(new SessionExists({ session: header.id })), all];
27
+ if (Option.isSome(header.parent) && !all.has(header.parent.value.id))
28
+ return [Effect.fail(missing(header.parent.value.id)), all];
29
+ const head = { header: copyHeader(header), seq: 0, revision: 0, state: {}, updatedAt: at };
30
+ return [Effect.succeed(copyHead(head, at)), new Map([...all, [header.id, { head, events: [] }]])];
31
+ })), Effect.flatten),
32
+ head: (id) => Effect.gen(function* () {
33
+ const at = yield* now, session = (yield* Ref.get(store)).get(id);
34
+ return session === void 0 ? yield* Effect.fail(missing(id)) : copyHead(session.head, at);
35
+ }),
36
+ list: (query) => Effect.gen(function* () {
37
+ const at = yield* now;
38
+ return [...(yield* Ref.get(store)).values()].map((session) => session.head).filter((head) => head.header.owner === query.owner).filter((head) => Option.match(query.parent, {
39
+ onNone: () => Option.isNone(head.header.parent),
40
+ onSome: (parent) => Option.exists(head.header.parent, (lineage) => lineage.id === parent)
41
+ })).filter(Option.match(query.before, { onNone: () => () => !0, onSome: olderThan })).sort((left, right) => right.updatedAt - left.updatedAt || (right.header.id < left.header.id ? -1 : right.header.id > left.header.id ? 1 : 0)).slice(0, Math.max(0, query.limit)).map((head) => copyHead(head, at));
42
+ }),
43
+ read: (id, query) => Ref.get(store).pipe(Effect.flatMap((all) => {
44
+ const session = all.get(id);
45
+ if (session === void 0)
46
+ return Effect.fail(missing(id));
47
+ const events = session.events.filter((event) => event.seq > query.after && (query.kinds.length === 0 || query.kinds.includes(event.kind)));
48
+ return Effect.succeed(Option.match(query.limit, { onNone: () => events, onSome: (limit) => events.slice(0, Math.max(0, limit)) }).map(copyEvent));
49
+ })),
50
+ commit: (id, commit) => now.pipe(Effect.flatMap((at) => Ref.modify(store, (all) => {
51
+ const session = all.get(id);
52
+ if (session === void 0)
53
+ return [Effect.fail(missing(id)), all];
54
+ if (session.head.revision !== commit.expect)
55
+ return [Effect.fail(new RevisionConflict({ session: id, expected: commit.expect, actual: session.head.revision })), all];
56
+ if (Option.isSome(commit.notAfter) && at > commit.notAfter.value)
57
+ return [Effect.fail(new LeaseExpired({ session: id, notAfter: commit.notAfter.value, now: at })), all];
58
+ const events = commit.events.map((draft, index) => ({
59
+ session: id,
60
+ seq: session.head.seq + index + 1,
61
+ turn: Option.map(draft.turn, (turn) => turn),
62
+ kind: draft.kind,
63
+ at,
64
+ data: stored(draft.data)
65
+ })), head = {
66
+ ...session.head,
67
+ seq: session.head.seq + events.length,
68
+ revision: session.head.revision + 1,
69
+ state: Option.match(commit.state, { onNone: () => session.head.state, onSome: stored }),
70
+ updatedAt: at
71
+ };
72
+ return [
73
+ Effect.succeed({ revision: head.revision, seq: head.seq, events: events.map(copyEvent), at }),
74
+ new Map([...all, [id, { head, events: [...session.events, ...events] }]])
75
+ ];
76
+ })), Effect.flatten),
77
+ remove: (id) => Ref.update(store, (all) => {
78
+ const removed = withDescendants(all, new Set([id]));
79
+ return new Map([...all].filter(([key]) => !removed.has(key)));
80
+ })
81
+ });
82
+ }));
@@ -0,0 +1 @@
1
+ export {};