@kairos-es/feed 0.0.0-stage → 0.0.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 (132) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +109 -2
  3. package/dist/cjs/Feed.js +6 -0
  4. package/dist/cjs/Feed.js.map +1 -0
  5. package/dist/cjs/FeedSink.js +54 -0
  6. package/dist/cjs/FeedSink.js.map +1 -0
  7. package/dist/cjs/FeedSyncStore.js +10 -0
  8. package/dist/cjs/FeedSyncStore.js.map +1 -0
  9. package/dist/cjs/deliver.js +86 -0
  10. package/dist/cjs/deliver.js.map +1 -0
  11. package/dist/cjs/errors.js +114 -0
  12. package/dist/cjs/errors.js.map +1 -0
  13. package/dist/cjs/identifiers.js +63 -0
  14. package/dist/cjs/identifiers.js.map +1 -0
  15. package/dist/cjs/inMemoryFeed.js +328 -0
  16. package/dist/cjs/inMemoryFeed.js.map +1 -0
  17. package/dist/cjs/inMemorySink.js +142 -0
  18. package/dist/cjs/inMemorySink.js.map +1 -0
  19. package/dist/cjs/index.js +184 -0
  20. package/dist/cjs/index.js.map +1 -0
  21. package/dist/cjs/inspect.js +68 -0
  22. package/dist/cjs/inspect.js.map +1 -0
  23. package/dist/cjs/readiness.js +100 -0
  24. package/dist/cjs/readiness.js.map +1 -0
  25. package/dist/cjs/reconcile.js +164 -0
  26. package/dist/cjs/reconcile.js.map +1 -0
  27. package/dist/cjs/runFeedSync.js +603 -0
  28. package/dist/cjs/runFeedSync.js.map +1 -0
  29. package/dist/cjs/tuning.js +60 -0
  30. package/dist/cjs/tuning.js.map +1 -0
  31. package/dist/dts-cjs/Feed.d.ts +99 -0
  32. package/dist/dts-cjs/Feed.d.ts.map +1 -0
  33. package/dist/dts-cjs/FeedSink.d.ts +189 -0
  34. package/dist/dts-cjs/FeedSink.d.ts.map +1 -0
  35. package/dist/dts-cjs/FeedSyncStore.d.ts +281 -0
  36. package/dist/dts-cjs/FeedSyncStore.d.ts.map +1 -0
  37. package/dist/dts-cjs/deliver.d.ts +16 -0
  38. package/dist/dts-cjs/deliver.d.ts.map +1 -0
  39. package/dist/dts-cjs/errors.d.ts +131 -0
  40. package/dist/dts-cjs/errors.d.ts.map +1 -0
  41. package/dist/dts-cjs/identifiers.d.ts +67 -0
  42. package/dist/dts-cjs/identifiers.d.ts.map +1 -0
  43. package/dist/dts-cjs/inMemoryFeed.d.ts +32 -0
  44. package/dist/dts-cjs/inMemoryFeed.d.ts.map +1 -0
  45. package/dist/dts-cjs/inMemorySink.d.ts +61 -0
  46. package/dist/dts-cjs/inMemorySink.d.ts.map +1 -0
  47. package/dist/dts-cjs/index.d.ts +63 -0
  48. package/dist/dts-cjs/index.d.ts.map +1 -0
  49. package/dist/dts-cjs/inspect.d.ts +71 -0
  50. package/dist/dts-cjs/inspect.d.ts.map +1 -0
  51. package/dist/dts-cjs/package.json +3 -0
  52. package/dist/dts-cjs/readiness.d.ts +84 -0
  53. package/dist/dts-cjs/readiness.d.ts.map +1 -0
  54. package/dist/dts-cjs/reconcile.d.ts +68 -0
  55. package/dist/dts-cjs/reconcile.d.ts.map +1 -0
  56. package/dist/dts-cjs/runFeedSync.d.ts +220 -0
  57. package/dist/dts-cjs/runFeedSync.d.ts.map +1 -0
  58. package/dist/dts-cjs/tuning.d.ts +40 -0
  59. package/dist/dts-cjs/tuning.d.ts.map +1 -0
  60. package/dist/dts-esm/Feed.d.ts +99 -0
  61. package/dist/dts-esm/Feed.d.ts.map +1 -0
  62. package/dist/dts-esm/FeedSink.d.ts +189 -0
  63. package/dist/dts-esm/FeedSink.d.ts.map +1 -0
  64. package/dist/dts-esm/FeedSyncStore.d.ts +281 -0
  65. package/dist/dts-esm/FeedSyncStore.d.ts.map +1 -0
  66. package/dist/dts-esm/deliver.d.ts +16 -0
  67. package/dist/dts-esm/deliver.d.ts.map +1 -0
  68. package/dist/dts-esm/errors.d.ts +131 -0
  69. package/dist/dts-esm/errors.d.ts.map +1 -0
  70. package/dist/dts-esm/identifiers.d.ts +67 -0
  71. package/dist/dts-esm/identifiers.d.ts.map +1 -0
  72. package/dist/dts-esm/inMemoryFeed.d.ts +32 -0
  73. package/dist/dts-esm/inMemoryFeed.d.ts.map +1 -0
  74. package/dist/dts-esm/inMemorySink.d.ts +61 -0
  75. package/dist/dts-esm/inMemorySink.d.ts.map +1 -0
  76. package/dist/dts-esm/index.d.ts +63 -0
  77. package/dist/dts-esm/index.d.ts.map +1 -0
  78. package/dist/dts-esm/inspect.d.ts +71 -0
  79. package/dist/dts-esm/inspect.d.ts.map +1 -0
  80. package/dist/dts-esm/package.json +3 -0
  81. package/dist/dts-esm/readiness.d.ts +84 -0
  82. package/dist/dts-esm/readiness.d.ts.map +1 -0
  83. package/dist/dts-esm/reconcile.d.ts +68 -0
  84. package/dist/dts-esm/reconcile.d.ts.map +1 -0
  85. package/dist/dts-esm/runFeedSync.d.ts +220 -0
  86. package/dist/dts-esm/runFeedSync.d.ts.map +1 -0
  87. package/dist/dts-esm/tuning.d.ts +40 -0
  88. package/dist/dts-esm/tuning.d.ts.map +1 -0
  89. package/dist/esm/Feed.js +2 -0
  90. package/dist/esm/Feed.js.map +1 -0
  91. package/dist/esm/FeedSink.js +46 -0
  92. package/dist/esm/FeedSink.js.map +1 -0
  93. package/dist/esm/FeedSyncStore.js +3 -0
  94. package/dist/esm/FeedSyncStore.js.map +1 -0
  95. package/dist/esm/deliver.js +78 -0
  96. package/dist/esm/deliver.js.map +1 -0
  97. package/dist/esm/errors.js +98 -0
  98. package/dist/esm/errors.js.map +1 -0
  99. package/dist/esm/identifiers.js +56 -0
  100. package/dist/esm/identifiers.js.map +1 -0
  101. package/dist/esm/inMemoryFeed.js +320 -0
  102. package/dist/esm/inMemoryFeed.js.map +1 -0
  103. package/dist/esm/inMemorySink.js +134 -0
  104. package/dist/esm/inMemorySink.js.map +1 -0
  105. package/dist/esm/index.js +62 -0
  106. package/dist/esm/index.js.map +1 -0
  107. package/dist/esm/inspect.js +58 -0
  108. package/dist/esm/inspect.js.map +1 -0
  109. package/dist/esm/package.json +4 -0
  110. package/dist/esm/readiness.js +92 -0
  111. package/dist/esm/readiness.js.map +1 -0
  112. package/dist/esm/reconcile.js +153 -0
  113. package/dist/esm/reconcile.js.map +1 -0
  114. package/dist/esm/runFeedSync.js +595 -0
  115. package/dist/esm/runFeedSync.js.map +1 -0
  116. package/dist/esm/tuning.js +51 -0
  117. package/dist/esm/tuning.js.map +1 -0
  118. package/package.json +34 -3
  119. package/src/Feed.ts +111 -0
  120. package/src/FeedSink.ts +130 -0
  121. package/src/FeedSyncStore.ts +326 -0
  122. package/src/deliver.ts +108 -0
  123. package/src/errors.ts +134 -0
  124. package/src/identifiers.ts +92 -0
  125. package/src/inMemoryFeed.ts +582 -0
  126. package/src/inMemorySink.ts +215 -0
  127. package/src/index.ts +136 -0
  128. package/src/inspect.ts +114 -0
  129. package/src/readiness.ts +137 -0
  130. package/src/reconcile.ts +224 -0
  131. package/src/runFeedSync.ts +882 -0
  132. package/src/tuning.ts +65 -0
@@ -0,0 +1,16 @@
1
+ import type { FeedChange } from './FeedSink.js';
2
+ import type { ParkedRecord } from './FeedSyncStore.js';
3
+ import type { FeedKey, FeedSeq } from './identifiers.js';
4
+ /** One change to send, and the `seq` of the row it came from (for its park). */
5
+ export interface Pending<D> {
6
+ readonly change: FeedChange<D>;
7
+ readonly seq: FeedSeq;
8
+ }
9
+ /** What a delivered batch came to: the keys the sink took, and the parks. */
10
+ export interface Delivered {
11
+ readonly sent: ReadonlyArray<FeedKey>;
12
+ readonly parked: ReadonlyArray<ParkedRecord>;
13
+ }
14
+ /** The reason recorded for a refused key the sink gave no reason for. */
15
+ export declare const UNEXPLAINED_REFUSAL = "refused by the sink, with no reason given";
16
+ //# sourceMappingURL=deliver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deliver.d.ts","sourceRoot":"","sources":["../../src/deliver.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,KAAK,EAAa,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAErD,gFAAgF;AAChF,MAAM,WAAW,OAAO,CAAC,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;IAC9B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;CACtB;AAED,6EAA6E;AAC7E,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IACrC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,YAAY,CAAC,CAAA;CAC7C;AAED,yEAAyE;AACzE,eAAO,MAAM,mBAAmB,8CAA8C,CAAA"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The change feed's error vocabulary.
3
+ *
4
+ * ## The sink's three answers
5
+ *
6
+ * A sink classifies every failure into one of three, and the driver's whole
7
+ * behaviour on failure follows from which. The names borrow the meaning of
8
+ * `@kairos-es/automation`'s classification without depending on that package: a
9
+ * feed is not an automation (ADR-0019), and a dependency for three class names
10
+ * would make it look like one.
11
+ *
12
+ * - `SinkRetryable` — the sink could not take the batch NOW (a rate limit, a 5xx, a
13
+ * timeout). The cursor stays where it is and the batch is sent again after a
14
+ * back-off, never sooner than `retryAfter` when the sink names one.
15
+ * - `SinkRejected` — the sink will never take THESE records as they stand (too
16
+ * large, invalid). The named keys are parked with the sink's reasons, the rest of
17
+ * the batch is sent again without them, and the cursor moves past the whole page.
18
+ * A parked record is retried when its row next changes.
19
+ * - `SinkHalted` — something is wrong that retrying will not mend (bad
20
+ * credentials, a missing index). The worker stops, and supervision decides.
21
+ *
22
+ * ## The feed's own tables
23
+ *
24
+ * `FeedWriteFailed` and `FeedUnreadable` are infrastructure faults on the feed's
25
+ * own durable state, split by direction: a write that did not land, a read that
26
+ * could not be made. They are tagged errors rather than defects for the reason the
27
+ * `ProjectionStore` port gives for its own: the driver's supervision has to tell
28
+ * "back off and try again" from "give up", and only a value can carry that.
29
+ *
30
+ * ## The worker's lease and cursor
31
+ *
32
+ * `LeaseHeld`, `LeaseLost` and `CursorSuperseded` are the three ways the
33
+ * single-worker guarantee shows itself: another worker holds the lease, this
34
+ * worker's lease went while it ran, or the cursor it expected to advance is not
35
+ * the one stored (a rival moved it, or the lease changed hands).
36
+ */
37
+ import { Duration } from 'effect';
38
+ import type { NonEmptyReadonlyArray } from 'effect/Array';
39
+ import type { FeedKey, FeedName, FeedSinkId, SinkName } from './identifiers.js';
40
+ declare const SinkRetryable_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
41
+ readonly _tag: "SinkRetryable";
42
+ } & Readonly<A>;
43
+ /** The sink cannot take the batch now: keep the cursor and back off. */
44
+ export declare class SinkRetryable extends SinkRetryable_base<{
45
+ readonly reason: string;
46
+ /** The least the driver waits before sending again, when the sink knows it. */
47
+ readonly retryAfter?: Duration.Duration;
48
+ }> {
49
+ get message(): string;
50
+ }
51
+ declare const SinkRejected_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
52
+ readonly _tag: "SinkRejected";
53
+ } & Readonly<A>;
54
+ /** The sink refuses these records as they stand: park them, send the rest. */
55
+ export declare class SinkRejected extends SinkRejected_base<{
56
+ readonly keys: NonEmptyReadonlyArray<FeedKey>;
57
+ /** The sink's reason per refused key; a key absent here is parked unexplained. */
58
+ readonly reasons: ReadonlyMap<FeedKey, string>;
59
+ }> {
60
+ get message(): string;
61
+ }
62
+ declare const SinkHalted_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
63
+ readonly _tag: "SinkHalted";
64
+ } & Readonly<A>;
65
+ /** Something retrying will not mend: stop the worker. */
66
+ export declare class SinkHalted extends SinkHalted_base<{
67
+ readonly reason: string;
68
+ }> {
69
+ get message(): string;
70
+ }
71
+ declare const FeedWriteFailed_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
72
+ readonly _tag: "FeedWriteFailed";
73
+ } & Readonly<A>;
74
+ /** A write to the feed's own tables did not land. */
75
+ export declare class FeedWriteFailed extends FeedWriteFailed_base<{
76
+ readonly reason: string;
77
+ readonly cause?: unknown;
78
+ }> {
79
+ get message(): string;
80
+ }
81
+ declare const FeedUnreadable_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
82
+ readonly _tag: "FeedUnreadable";
83
+ } & Readonly<A>;
84
+ /** A read of the feed's own tables could not be made. */
85
+ export declare class FeedUnreadable extends FeedUnreadable_base<{
86
+ readonly reason: string;
87
+ readonly cause?: unknown;
88
+ }> {
89
+ get message(): string;
90
+ }
91
+ declare const LeaseHeld_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
92
+ readonly _tag: "LeaseHeld";
93
+ } & Readonly<A>;
94
+ /** Another worker holds the lease for this feed and sink. */
95
+ export declare class LeaseHeld extends LeaseHeld_base<FeedSinkId> {
96
+ get message(): string;
97
+ }
98
+ declare const LeaseLost_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
99
+ readonly _tag: "LeaseLost";
100
+ } & Readonly<A>;
101
+ /** This worker's lease is no longer its own; it must stop sending at once. */
102
+ export declare class LeaseLost extends LeaseLost_base<FeedSinkId> {
103
+ get message(): string;
104
+ }
105
+ declare const CursorSuperseded_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
106
+ readonly _tag: "CursorSuperseded";
107
+ } & Readonly<A>;
108
+ /**
109
+ * The cursor compare-and-set lost: the stored cursor is not the one expected, or
110
+ * the lease is not held by the worker that asked. Nothing the call carried landed.
111
+ */
112
+ export declare class CursorSuperseded extends CursorSuperseded_base<FeedSinkId> {
113
+ get message(): string;
114
+ }
115
+ declare const FeedSyncStalled_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
116
+ readonly _tag: "FeedSyncStalled";
117
+ } & Readonly<A>;
118
+ /**
119
+ * The sync worker gave up: it restarted `restarts` times in a row without moving
120
+ * its cursor once. `reason` is the failure that ended the last attempt.
121
+ */
122
+ export declare class FeedSyncStalled extends FeedSyncStalled_base<{
123
+ readonly feed: FeedName;
124
+ readonly sink: SinkName;
125
+ readonly restarts: number;
126
+ readonly reason: unknown;
127
+ }> {
128
+ get message(): string;
129
+ }
130
+ export {};
131
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAQ,QAAQ,EAAE,MAAM,QAAQ,CAAA;AACvC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;;;;AAE5E,wEAAwE;AACxE,qBAAa,aAAc,SAAQ,mBAAkC;IACnE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAA;CACxC,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAI7B;CACF;;;;AAED,8EAA8E;AAC9E,qBAAa,YAAa,SAAQ,kBAAiC;IACjE,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC,OAAO,CAAC,CAAA;IAC7C,kFAAkF;IAClF,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;CAC/C,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;;;;AAED,yDAAyD;AACzD,qBAAa,UAAW,SAAQ,gBAA+B;IAC7D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;;;;AAED,qDAAqD;AACrD,qBAAa,eAAgB,SAAQ,qBAAoC;IACvE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CACzB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;;;;AAED,yDAAyD;AACzD,qBAAa,cAAe,SAAQ,oBAAmC;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CACzB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;;;;AAED,6DAA6D;AAC7D,qBAAa,SAAU,SAAQ,eAA8B,UAAU,CAAC;IACtE,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;;;;AAED,8EAA8E;AAC9E,qBAAa,SAAU,SAAQ,eAA8B,UAAU,CAAC;IACtE,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;;;;AAED;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,sBAEpC,UAAU,CAAC;IACX,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;;;;AAED;;;GAGG;AACH,qBAAa,eAAgB,SAAQ,qBAAoC;IACvE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CACzB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The change feed's branded identifiers.
3
+ *
4
+ * Every one of them is a branded `Schema`, as every domain identifier in this
5
+ * repository is, so a raw string or `bigint` cannot stand in for one by accident.
6
+ * Two of them are worth a word, because they look like things the read side
7
+ * already has and are deliberately not those things.
8
+ *
9
+ * - `FeedSeq` is NOT a `Position`. A `Position` is a place in the global event log,
10
+ * and one event may fan out into many feed rows (a teacher renamed, fifty courses
11
+ * touched). Give those fifty rows the event's position as their cursor and they
12
+ * all share one value, so a page boundary that falls among them loses the rest of
13
+ * them: the next page asks for rows strictly after a value the lost ones also
14
+ * carry. A feed-local sequence gives every row a cursor value of its own. The
15
+ * event position is still kept on each row, as `eventPosition`, for the one
16
+ * question it answers — "is the sink at least as fresh as P?" — and is never a
17
+ * cursor.
18
+ * - `Generation` is NOT a counter a caller reads meaning into. It is compared for
19
+ * EQUALITY only: a sink whose cursor was saved at a generation other than the
20
+ * feed's current one resyncs. That is what lets the in-memory feed draw a fresh
21
+ * generation from `Random` on every boot while a durable feed counts upwards, and
22
+ * both satisfy the one rule the driver needs.
23
+ */
24
+ import { Schema } from 'effect';
25
+ /**
26
+ * The name of one change feed. A feed is bound to ONE materialisation, because a
27
+ * feed sequence taken inside a commit is ascending in commit order only while one
28
+ * transaction at a time writes that feed — which the read side gives per
29
+ * materialisation and no wider.
30
+ */
31
+ export declare const FeedName: Schema.brand<Schema.String, "FeedName">;
32
+ export type FeedName = typeof FeedName.Type;
33
+ /** A record's key within a feed, and the sink's object id for that record. */
34
+ export declare const FeedKey: Schema.brand<Schema.String, "FeedKey">;
35
+ export type FeedKey = typeof FeedKey.Type;
36
+ /**
37
+ * The feed-local cursor: unique, and ascending in commit order. Compared, never
38
+ * arithmetic'd, and never assumed contiguous — a sequence shared by several feeds
39
+ * leaves gaps in each, and a coalesced row gives up its old value when it moves.
40
+ */
41
+ export declare const FeedSeq: Schema.brand<Schema.BigInt, "FeedSeq">;
42
+ export type FeedSeq = typeof FeedSeq.Type;
43
+ /** The sentinel feed sequence meaning "before the first row". */
44
+ export declare const FEED_ORIGIN: FeedSeq;
45
+ /**
46
+ * The feed's generation: it moves on every reset of the materialisation the feed
47
+ * is bound to, and on every boot of an ephemeral feed. Compared for equality only;
48
+ * see the module doc.
49
+ */
50
+ export declare const Generation: Schema.brand<Schema.BigInt, "Generation">;
51
+ export type Generation = typeof Generation.Type;
52
+ /** The name of one sink. A feed keeps one cursor per sink. */
53
+ export declare const SinkName: Schema.brand<Schema.String, "SinkName">;
54
+ export type SinkName = typeof SinkName.Type;
55
+ /**
56
+ * Who holds a sync worker's lease. Issued by the `FeedSyncStore` when the lease is
57
+ * taken, and checked again by every cursor compare-and-set, so a worker that has
58
+ * lost its lease cannot move the cursor even if it is still running.
59
+ */
60
+ export declare const LeaseHolder: Schema.brand<Schema.String, "LeaseHolder">;
61
+ export type LeaseHolder = typeof LeaseHolder.Type;
62
+ /** One sync worker's identity: the feed it reads and the sink it writes. */
63
+ export interface FeedSinkId {
64
+ readonly feed: FeedName;
65
+ readonly sink: SinkName;
66
+ }
67
+ //# sourceMappingURL=identifiers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identifiers.d.ts","sourceRoot":"","sources":["../../src/identifiers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/B;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,yCAGpB,CAAA;AACD,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C,8EAA8E;AAC9E,eAAO,MAAM,OAAO,wCAGnB,CAAA;AACD,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAEzC;;;;GAIG;AACH,eAAO,MAAM,OAAO,wCAGnB,CAAA;AACD,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAEzC,iEAAiE;AACjE,eAAO,MAAM,WAAW,EAAE,OAA0B,CAAA;AAEpD;;;;GAIG;AACH,eAAO,MAAM,UAAU,2CAGtB,CAAA;AACD,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,IAAI,CAAA;AAE/C,8DAA8D;AAC9D,eAAO,MAAM,QAAQ,yCAGpB,CAAA;AACD,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C;;;;GAIG;AACH,eAAO,MAAM,WAAW,4CAGvB,CAAA;AACD,MAAM,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,IAAI,CAAA;AAEjD,4EAA4E;AAC5E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CACxB"}
@@ -0,0 +1,32 @@
1
+ import type { CheckpointKey, ProjectionStore } from '@kairos-es/read';
2
+ import { Effect, Schema } from 'effect';
3
+ import type { BatchedFeedWriter, FeedSource } from './Feed.js';
4
+ import { type FeedSyncStore } from './FeedSyncStore.js';
5
+ import { type FeedName } from './identifiers.js';
6
+ /** The in-memory feed; see the module doc. */
7
+ export interface InMemoryFeed<D> extends BatchedFeedWriter<D> {
8
+ readonly source: FeedSource<D>;
9
+ readonly syncStore: FeedSyncStore;
10
+ }
11
+ /** Options for {@link makeInMemoryFeed}. */
12
+ export interface InMemoryFeedOptions<D, I> {
13
+ readonly feed: FeedName;
14
+ /** The document's schema: what an upsert encodes, and compares, through. */
15
+ readonly doc: Schema.Codec<D, I>;
16
+ }
17
+ /** Build an in-memory feed at a fresh generation; see the module doc. */
18
+ export declare const makeInMemoryFeed: <D, I>(options: InMemoryFeedOptions<D, I>) => Effect.Effect<InMemoryFeed<D>>;
19
+ /**
20
+ * `store`, with `resetCheckpoint` for `key` replaced by one that also empties
21
+ * `feed` and moves its generation on, so every sink resyncs once the read model is
22
+ * replayed. Every other key passes through untouched, and `readCheckpoint` and
23
+ * `commit` are the store's own, so the result works through `forKey` exactly as
24
+ * the store does.
25
+ *
26
+ * Binding is checked here, at construction: a feed serves ONE materialisation,
27
+ * because one writer is what keeps its sequence ascending in commit order, so a
28
+ * feed already bound to another checkpoint key is refused, as is a durable `store`
29
+ * (see the module doc). Both refusals are defects.
30
+ */
31
+ export declare const withInMemoryFeed: <D>(store: ProjectionStore, feed: InMemoryFeed<D>, key: CheckpointKey) => Effect.Effect<ProjectionStore>;
32
+ //# sourceMappingURL=inMemoryFeed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inMemoryFeed.d.ts","sourceRoot":"","sources":["../../src/inMemoryFeed.ts"],"names":[],"mappings":"AA8CA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACrE,OAAO,EAEL,MAAM,EAKN,MAAM,EAEP,MAAM,QAAQ,CAAA;AAEf,OAAO,KAAK,EAAE,iBAAiB,EAAW,UAAU,EAAc,MAAM,QAAQ,CAAA;AAChF,OAAO,EAIL,KAAK,aAAa,EAKnB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAGL,KAAK,QAAQ,EAMd,MAAM,eAAe,CAAA;AAEtB,8CAA8C;AAC9C,MAAM,WAAW,YAAY,CAAC,CAAC,CAAE,SAAQ,iBAAiB,CAAC,CAAC,CAAC;IAC3D,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAA;CAClC;AAsDD,4CAA4C;AAC5C,MAAM,WAAW,mBAAmB,CAAC,CAAC,EAAE,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;CACjC;AAED,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,GAAI,CAAC,EAAE,CAAC,EACnC,SAAS,mBAAmB,CAAC,CAAC,EAAE,CAAC,CAAC,KACjC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CA0W5B,CAAA;AAwBJ;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,EAChC,OAAO,eAAe,EACtB,MAAM,YAAY,CAAC,CAAC,CAAC,EACrB,KAAK,aAAa,KACjB,MAAM,CAAC,MAAM,CAAC,eAAe,CA+B5B,CAAA"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The in-memory sink: a `FeedSink` over a `Ref` of documents, with an optional
3
+ * `FeedResync` and `FeedReconcile`, and the ORACLE the sink law suite holds every
4
+ * other sink against.
5
+ *
6
+ * It keeps the laws by construction: an upsert replaces the whole document, a
7
+ * delete of an absent key is a no-op, so the same batch applied twice leaves it as
8
+ * once; a batch is visible the moment `apply` returns, so `confirm` has nothing to
9
+ * wait for; a resync target is a separate map that `publish` swaps in whole, so a
10
+ * published resync holds exactly what was loaded into it, and a load into a target
11
+ * a later `begin` dropped fails rather than recreating it. Each `begin` names its
12
+ * target by a counter the sink's own state holds, so two workers sharing one sink
13
+ * value — the way two processes share one real index — never share a target, and
14
+ * the name is the same on every run of a test.
15
+ *
16
+ * Its two optional parts are switched on by option rather than always present,
17
+ * because which parts a sink has CHANGES what the driver does — a sink with no
18
+ * `resync` is resynced in place — and a test of either path needs a sink that
19
+ * lacks the other. It also records every batch it is handed, live or into a
20
+ * target, so a case can say how many sends a change cost.
21
+ *
22
+ * It lives for as long as the value is held, independently of any feed, which is
23
+ * what lets a test keep one sink across two in-memory feeds and watch the second
24
+ * feed's fresh generation force a resync.
25
+ */
26
+ import { Effect } from 'effect';
27
+ import type { NonEmptyReadonlyArray } from 'effect/Array';
28
+ import type { FeedChange, FeedSink } from './FeedSink.js';
29
+ import type { FeedKey, Generation, SinkName } from './identifiers.js';
30
+ /** What the in-memory sink's `apply` and `load` return: where, and which batch. */
31
+ export interface InMemoryReceipt {
32
+ /** `'live'`, or the name of the resync target the batch was loaded into. */
33
+ readonly target: string;
34
+ readonly batch: number;
35
+ }
36
+ /** One batch the sink was handed, and where it went. */
37
+ export interface InMemorySentBatch<D> {
38
+ readonly target: string;
39
+ readonly changes: NonEmptyReadonlyArray<FeedChange<D>>;
40
+ }
41
+ /** Options for {@link makeInMemorySink}. */
42
+ export interface InMemorySinkOptions {
43
+ readonly name: SinkName;
44
+ /** Give the sink a `FeedResync` (an atomic swap). */
45
+ readonly resync?: boolean;
46
+ /** Give the sink a `FeedReconcile` (a key listing). */
47
+ readonly reconcile?: boolean;
48
+ }
49
+ /** The in-memory sink and what a test reads back from it. */
50
+ export interface InMemorySink<D> {
51
+ readonly sink: FeedSink<D, InMemoryReceipt>;
52
+ /** The live documents, by key. */
53
+ readonly documents: Effect.Effect<ReadonlyMap<FeedKey, D>>;
54
+ /** Every batch handed to `apply` or `load`, in order. */
55
+ readonly batches: Effect.Effect<ReadonlyArray<InMemorySentBatch<D>>>;
56
+ /** The generation of every resync published, in order. */
57
+ readonly published: Effect.Effect<ReadonlyArray<Generation>>;
58
+ }
59
+ /** Build an in-memory sink; see the module doc. */
60
+ export declare const makeInMemorySink: <D>(options: InMemorySinkOptions) => Effect.Effect<InMemorySink<D>>;
61
+ //# sourceMappingURL=inMemorySink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inMemorySink.d.ts","sourceRoot":"","sources":["../../src/inMemorySink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,MAAM,EAAe,MAAM,QAAQ,CAAA;AAC5C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAEzD,OAAO,KAAK,EAAE,UAAU,EAAc,QAAQ,EAAgB,MAAM,YAAY,CAAA;AAChF,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAElE,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED,wDAAwD;AACxD,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;CACvD;AAED,4CAA4C;AAC5C,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,qDAAqD;IACrD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB,uDAAuD;IACvD,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAC7B;AAED,6DAA6D;AAC7D,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,EAAE,eAAe,CAAC,CAAA;IAC3C,kCAAkC;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAA;IAC1D,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACpE,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,CAAA;CAC7D;AAsCD,mDAAmD;AACnD,eAAO,MAAM,gBAAgB,GAAI,CAAC,EAChC,SAAS,mBAAmB,KAC3B,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CA+G5B,CAAA"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @kairos-es/feed — a state-only change feed: the newest state of each record a
3
+ * read model touches, written inside that read model's own commit, and copied by
4
+ * a sync worker to a sink that has no transaction of its own — a search index, a
5
+ * cache, a mirror (ADR-0019).
6
+ *
7
+ * The feed copies STATE. It never tells anyone each thing that happened; that is
8
+ * `@kairos-es/automation`'s job, and the two are deliberately different shapes: an
9
+ * automation's only progress record is the log, while a feed keeps a cursor, a
10
+ * lease and a parked list beside its rows, because what it mirrors is state and
11
+ * its progress is legitimately a cursor.
12
+ *
13
+ * What is here, and where each part's argument lives:
14
+ *
15
+ * - The identifiers (`identifiers.ts`) and the error vocabulary (`errors.ts`).
16
+ * - The feed's write and read sides, `BatchedFeedWriter` / `FeedWriter` and
17
+ * `FeedSource` (`Feed.ts`): why the feed is written inside the commit, and what
18
+ * "coalesced, latest wins" means for a stored row.
19
+ * - The sink port, `FeedSink` with its optional `FeedResync` and `FeedReconcile`
20
+ * (`FeedSink.ts`): what a sink must keep, and why a sink with neither optional
21
+ * part is refused.
22
+ * - The sync store port, `FeedSyncStore` (`FeedSyncStore.ts`): the cursor, the
23
+ * parked list and the worker lease, and the exact contract a backend implements.
24
+ * - The driver, `runFeedSync` (`runFeedSync.ts`): the lifecycle, the rules it
25
+ * keeps, resync, and its own supervision.
26
+ * - `FeedReadiness` and `materialisationCaughtUp` (`readiness.ts`): why a
27
+ * rebuild's publish can wait for the materialisation, and what it waits on.
28
+ * - `reconcileFeed` (`reconcile.ts`), and `inspectFeed` / `retryParked` /
29
+ * `removeSink` (`inspect.ts`).
30
+ * - The in-memory feed (`inMemoryFeed.ts`) and sink (`inMemorySink.ts`): the parity
31
+ * oracles, and what the in-memory feed refuses.
32
+ *
33
+ * Wiring, end to end, in memory:
34
+ *
35
+ * ```ts
36
+ * const feed = yield* makeInMemoryFeed({ feed: FeedName.make('course-search'), doc: CourseDoc })
37
+ * const store = yield* withInMemoryFeed(yield* makeInMemoryProjectionStore, feed, key)
38
+ *
39
+ * // In the read model's `apply`, as the LAST step of the batch:
40
+ * const apply = (batch) =>
41
+ * feed.write(Arr.lastNonEmpty(batch).sequenced.position, (writer) =>
42
+ * Effect.forEach(touchedCourses(batch), (course) =>
43
+ * writer.upsert(FeedKey.make(course.id), toDoc(course)),
44
+ * ),
45
+ * )
46
+ *
47
+ * // Beside the projections, in a fibre of its own:
48
+ * const { sink } = yield* makeInMemorySink<CourseDoc>({ name: SinkName.make('search'), resync: true })
49
+ * yield* Effect.forkScoped(runFeedSync({ source: feed.source, sink, store: feed.syncStore }))
50
+ * ```
51
+ */
52
+ export { CursorSuperseded, FeedSyncStalled, FeedUnreadable, FeedWriteFailed, LeaseHeld, LeaseLost, SinkHalted, SinkRejected, SinkRetryable, } from './errors.js';
53
+ export type { BatchedFeedWriter, FeedRow, FeedSource, FeedWriter, } from './Feed.js';
54
+ export { changeOf, FeedChange, type FeedChangeDefinition, type FeedReconcile, type FeedResync, type FeedSink, type ResyncTarget, } from './FeedSink.js';
55
+ export { type FeedCursor, type FeedLease, type FeedSyncDescription, type FeedSyncStore, type LeaseDescription, type ParkedChange, type ParkedRecord, type SinkSyncState, sameCursor, } from './FeedSyncStore.js';
56
+ export { FEED_ORIGIN, FeedKey, FeedName, FeedSeq, type FeedSinkId, Generation, LeaseHolder, SinkName, } from './identifiers.js';
57
+ export { type InMemoryFeed, type InMemoryFeedOptions, makeInMemoryFeed, withInMemoryFeed, } from './inMemoryFeed.js';
58
+ export { type InMemoryReceipt, type InMemorySentBatch, type InMemorySink, type InMemorySinkOptions, makeInMemorySink, } from './inMemorySink.js';
59
+ export { type FeedInspection, inspectFeed, removeSink, retryParked, type SinkInspection, } from './inspect.js';
60
+ export { type CaughtUpReadModel, type FeedReadiness, type FeedReadinessProbe, materialisationCaughtUp, } from './readiness.js';
61
+ export { type ReconcileFeedOptions, type ReconcileReport, reconcileFeed, } from './reconcile.js';
62
+ export { type FeedSyncRestart, type FeedSyncTuning, type RunFeedSyncOptions, runFeedSync, } from './runFeedSync.js';
63
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,eAAe,EACf,SAAS,EACT,SAAS,EACT,UAAU,EACV,YAAY,EACZ,aAAa,GACd,MAAM,UAAU,CAAA;AACjB,YAAY,EACV,iBAAiB,EACjB,OAAO,EACP,UAAU,EACV,UAAU,GACX,MAAM,QAAQ,CAAA;AACf,OAAO,EACL,QAAQ,EACR,UAAU,EACV,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,YAAY,GAClB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,UAAU,GACX,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,WAAW,EACX,OAAO,EACP,QAAQ,EACR,OAAO,EACP,KAAK,UAAU,EACf,UAAU,EACV,WAAW,EACX,QAAQ,GACT,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,gBAAgB,GACjB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACL,KAAK,cAAc,EACnB,WAAW,EACX,UAAU,EACV,WAAW,EACX,KAAK,cAAc,GACpB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,uBAAuB,GACxB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,aAAa,GACd,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,WAAW,GACZ,MAAM,eAAe,CAAA"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `inspectFeed`, `retryParked` and `removeSink`: an operator's view of a feed's
3
+ * sinks, and the two repairs an operator makes by hand.
4
+ *
5
+ * None of them needs the sink's client, because everything they read or change
6
+ * lives in the source database beside the feed: an operator can see a sink's lag,
7
+ * its lease, its parked records and when it last advanced, send a parked record
8
+ * again, and retire a sink, without the credentials of the index concerned. The
9
+ * same choice `@kairos-es/automation` made for its own `inspect`. There is no CLI
10
+ * or UI over any of them; they are effects an application wires into whatever it
11
+ * already has.
12
+ *
13
+ * `lastAdvance` and `removeSink` are a pair. Every sink registered on a feed holds
14
+ * its tombstone collection back, so a sink the application stopped running and
15
+ * never removed lets tombstones pile up for good; `lastAdvance` is how such a sink
16
+ * shows — still, while the head moves — and `removeSink` is what lets collection
17
+ * proceed without it. `FeedSyncStore.ts` owns both rules.
18
+ */
19
+ import { type DateTime, Effect, Option } from 'effect';
20
+ import type { FeedUnreadable, FeedWriteFailed, LeaseHeld } from './errors.js';
21
+ import type { FeedSource } from './Feed.js';
22
+ import type { FeedCursor, FeedSyncStore, LeaseDescription, ParkedRecord } from './FeedSyncStore.js';
23
+ import type { FeedKey, FeedName, FeedSeq, Generation, SinkName } from './identifiers.js';
24
+ /** One sink of a feed, as `inspectFeed` reports it. */
25
+ export interface SinkInspection {
26
+ readonly sink: SinkName;
27
+ /** The generation the sink's cursor was saved at; `None` before its first sync. */
28
+ readonly generation: Option.Option<Generation>;
29
+ readonly cursor: Option.Option<FeedCursor>;
30
+ /** When the cursor last moved; `None` if it never has. See `SinkSyncState.lastAdvance`. */
31
+ readonly lastAdvance: Option.Option<DateTime.Utc>;
32
+ /** True when the next pass will resync: no cursor, or one at another generation. */
33
+ readonly resyncDue: boolean;
34
+ /** Rows the sink has still to receive; see `SinkSyncState.pending`. */
35
+ readonly lag: number;
36
+ readonly lease: Option.Option<LeaseDescription>;
37
+ readonly parked: ReadonlyArray<ParkedRecord>;
38
+ }
39
+ /** A feed and its sinks. */
40
+ export interface FeedInspection {
41
+ readonly feed: FeedName;
42
+ readonly generation: Generation;
43
+ /** The highest `seq` the feed holds. */
44
+ readonly head: FeedSeq;
45
+ readonly sinks: ReadonlyArray<SinkInspection>;
46
+ }
47
+ /** Every sink of `source`'s feed: its cursor, lag, lease and parked records. */
48
+ export declare const inspectFeed: <D>(source: FeedSource<D>, store: FeedSyncStore) => Effect.Effect<FeedInspection, FeedUnreadable>;
49
+ /**
50
+ * Send one parked record again: delete its park and re-stamp its row's `seq`, so
51
+ * the next pass of every sink on the feed sends it. Succeeds with whether the key
52
+ * was parked at all. Every sink on the feed receives the record again, not only
53
+ * `sink`; an idempotent sink absorbs that.
54
+ */
55
+ export declare const retryParked: (store: FeedSyncStore, args: {
56
+ readonly feed: FeedName;
57
+ readonly sink: SinkName;
58
+ readonly key: FeedKey;
59
+ }) => Effect.Effect<boolean, FeedWriteFailed>;
60
+ /**
61
+ * Retire `sink` from `feed`: delete its cursor, its lease record and its parked
62
+ * records, so it no longer holds the feed's tombstone collection back. Succeeds
63
+ * with whether the store held anything for it. Fails `LeaseHeld`, removing
64
+ * nothing, while a worker for the sink is running: stop the worker first. A worker
65
+ * started for the sink later registers it again from scratch, and resyncs.
66
+ */
67
+ export declare const removeSink: (store: FeedSyncStore, args: {
68
+ readonly feed: FeedName;
69
+ readonly sink: SinkName;
70
+ }) => Effect.Effect<boolean, LeaseHeld | FeedWriteFailed>;
71
+ //# sourceMappingURL=inspect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/inspect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACxC,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,YAAY,EACb,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EACV,OAAO,EACP,QAAQ,EACR,OAAO,EACP,UAAU,EACV,QAAQ,EACT,MAAM,eAAe,CAAA;AAEtB,uDAAuD;AACvD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,mFAAmF;IACnF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IAC1C,2FAA2F;IAC3F,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;IACjD,oFAAoF;IACpF,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAA;IAC/C,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,YAAY,CAAC,CAAA;CAC7C;AAED,4BAA4B;AAC5B,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAA;IAC/B,wCAAwC;IACxC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,cAAc,CAAC,CAAA;CAC9C;AAED,gFAAgF;AAChF,eAAO,MAAM,WAAW,GAAI,CAAC,EAC3B,QAAQ,UAAU,CAAC,CAAC,CAAC,EACrB,OAAO,aAAa,KACnB,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,cAAc,CAsB3C,CAAA;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GACtB,OAAO,aAAa,EACpB,MAAM;IACJ,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;CACtB,KACA,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,eAAe,CAA4B,CAAA;AAErE;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GACrB,OAAO,aAAa,EACpB,MAAM;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,KACzD,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,GAAG,eAAe,CAA2B,CAAA"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * `FeedReadiness` — when a rebuilt copy may go live — and
3
+ * `materialisationCaughtUp`, the readiness of the common case.
4
+ *
5
+ * ## Why a rebuild has anything to wait for
6
+ *
7
+ * A reset empties the feed and moves its generation on, and the materialisation
8
+ * then replays the log, writing the feed afresh as it goes. The sync worker sees
9
+ * the new generation within a poll interval and starts its resync at once, but the
10
+ * feed it pages through is only as complete as the replay is far along. Reaching
11
+ * the feed's last row says nothing about the replay: it is wherever the replay had
12
+ * got to. So a resync that publishes at the feed's end swaps a complete live copy
13
+ * for whatever fraction of the rebuild the replay had written, and search serves
14
+ * that partial copy until the tail fills the rest in.
15
+ *
16
+ * Only the materialisation knows how far its replay has got, and the feed's ports
17
+ * deliberately name no materialisation, so the driver is TOLD, through an optional
18
+ * readiness signal on `runFeedSync`, and holds the step that makes the rebuild
19
+ * final until the signal says yes. With no signal the resync publishes at the
20
+ * feed's end, as it always did, and a rebuild can serve a partial copy for as long
21
+ * as the replay takes.
22
+ *
23
+ * ## Why it is a function returning an effect
24
+ *
25
+ * "Caught up" has to be relative to a fixed point: the log as it stood when the
26
+ * rebuild began. Judged against the log as it stands at each ask, a read model
27
+ * under steady writes is always a batch or two behind, and the publish would wait
28
+ * for a quiet moment that may never come. So the signal is asked ONCE per resync
29
+ * attempt, as the attempt starts, for the generation that attempt rebuilds, and
30
+ * returns the PROBE the worker then polls — by which time whatever point the probe
31
+ * judges by is fixed. An attempt starts after the reset that moved the generation,
32
+ * so a point taken then is at or after the log as it stood at the reset: the error
33
+ * is towards waiting slightly longer, never towards publishing early.
34
+ *
35
+ * Both halves fail `FeedUnreadable`, the feed's own "a read could not be made":
36
+ * the worker cannot tell a probe that could not read the materialisation's
37
+ * progress from a feed it could not read, and needs to do the same thing about
38
+ * either — end the session and try again after a back-off.
39
+ */
40
+ import { type AnyProjection, DcbEventStore } from '@kairos-es/core';
41
+ import type { KeyedProjectionStore } from '@kairos-es/read';
42
+ import { Effect } from 'effect';
43
+ import { FeedUnreadable } from './errors.js';
44
+ import type { Generation } from './identifiers.js';
45
+ /**
46
+ * Polled by the worker, between pages, until it answers `true`: the rebuild this
47
+ * probe was made for may now go live. See the module doc.
48
+ */
49
+ export type FeedReadinessProbe = Effect.Effect<boolean, FeedUnreadable>;
50
+ /**
51
+ * Asked once per resync attempt, as it starts, for the generation it rebuilds,
52
+ * and answering with the {@link FeedReadinessProbe} that attempt polls. See the
53
+ * module doc for why it is two steps.
54
+ */
55
+ export type FeedReadiness = (generation: Generation) => Effect.Effect<FeedReadinessProbe, FeedUnreadable>;
56
+ /**
57
+ * What {@link materialisationCaughtUp} needs of a read model: its bound view store
58
+ * and its slices. Structural, so any `ReadModel` from `@kairos-es/read` is one.
59
+ */
60
+ export interface CaughtUpReadModel {
61
+ readonly store: KeyedProjectionStore;
62
+ readonly slices: Readonly<Record<string, AnyProjection>>;
63
+ }
64
+ /**
65
+ * The readiness of the common case: the materialisation's checkpoint has reached
66
+ * the last event its query selects, as the log stood when the resync attempt
67
+ * started. Pass the very read model whose `apply` writes the feed, so its query
68
+ * and its checkpoint are the ones the replay moves.
69
+ *
70
+ * The point is the last event the read model's QUERY selects, and not the log's
71
+ * head, because the runner advances its checkpoint to the positions it is
72
+ * delivered and is never delivered an event its query does not select: a
73
+ * checkpoint judged against a head an unrelated event moved would never arrive.
74
+ * The store hands no cheaper way to that position than reading up to it, so each
75
+ * attempt starts with one paged read of the query's events past the checkpoint —
76
+ * the backlog the replay itself is about to read, a page at a time and never held
77
+ * whole — and each probe after it is one checkpoint read.
78
+ *
79
+ * Generation-blind, deliberately: the read model has one checkpoint whatever the
80
+ * feed's generation, and an attempt whose generation moves on while it waits is
81
+ * abandoned by the worker's own generation check, so the probe never has to know.
82
+ */
83
+ export declare const materialisationCaughtUp: (readModel: CaughtUpReadModel) => Effect.Effect<FeedReadiness, never, DcbEventStore>;
84
+ //# sourceMappingURL=readiness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readiness.d.ts","sourceRoot":"","sources":["../../src/readiness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,EACL,KAAK,aAAa,EAElB,aAAa,EAId,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,EAAgB,MAAM,EAAkB,MAAM,QAAQ,CAAA;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE/C;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,cAAc,CAAC,CAAA;AAEvE;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,UAAU,EAAE,UAAU,KACnB,MAAM,CAAC,MAAM,CAAC,kBAAkB,EAAE,cAAc,CAAC,CAAA;AAEtD;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAA;IACpC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAA;CACzD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,uBAAuB,GAClC,WAAW,iBAAiB,KAC3B,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,KAAK,EAAE,aAAa,CAuChD,CAAA"}