@rikalabs/akter 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2 (2026-10-15)
4
+
5
+ The framework and CLI release together at `0.2.2` on the `latest` npm dist-tag. Stores written by 0.2.0 and 0.2.1 open unchanged.
6
+
7
+ - A fork of a position older than every retained checkpoint is refused with `ForkRefused("retention")` once retention has trimmed its log prefix. It was reported as `CorruptSegment`, as if the store had lost data. A segment missing inside retention is still `CorruptSegment`.
8
+ - The retention refusal is decided from the newest position the oldest retained checkpoint covers (including a fork installed into its shard), so losing in-window segments through that checkpoint is still `CorruptSegment`.
9
+ - New public field: `HistoryCheckpoint` (exported from `@rikalabs/akter/server`) gains a required `expired` boolean. `Store.history.checkpoints` sets it when the checkpoint's offset precedes the oldest stored segment written within retention. It is informational; forks do not use it.
10
+
3
11
  ## 0.2.1 (2026-10-14)
4
12
 
5
13
  The framework and CLI release together at `0.2.1` on the `latest` npm dist-tag. Stores written by 0.2.0 open unchanged, and a publication epoch 0.2.0 left stranded expires on its own once a 0.2.1 runner or collector reaches it. Do not run `Store.collect` against a store while a 0.2.0 runner of it is still live.
@@ -16,7 +16,12 @@ export declare const STREAM_BUFFER = 64;
16
16
  declare const make: (engine: Engine, actors: ReadonlyArray<AnyActor>, sessions: Sessions, routing?: Sharding["Service"]) => {
17
17
  [k: string]: Rpc.ToHandlerFn<Rpc.Rpc<string, Schema.Struct<{
18
18
  readonly id: Schema.Codec<Schema.Json, Schema.Json, never, never>;
19
- }>, import("effect/rpc/RpcSchema").Stream<Schema.Codec<Schema.Json, Schema.Json, never, never>, Schema.decodeTo<typeof ActorError, Schema.TaggedStruct<"ActorError", {
19
+ readonly commandId: Schema.String;
20
+ readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
21
+ }>, Schema.Struct<{
22
+ readonly value: Schema.Codec<Schema.Json, Schema.Json, never, never>;
23
+ readonly replayed: Schema.Boolean;
24
+ }>, Schema.Union<readonly [Schema.decodeTo<typeof ActorError, Schema.TaggedStruct<"ActorError", {
20
25
  readonly reason: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Unauthorized", {
21
26
  readonly code: Schema.Literals<readonly ["missing_credentials", "invalid_credentials", "expired", "access_denied", "outcome_access_denied", "reauthorization_unavailable"]>;
22
27
  }>, Schema.TaggedStruct<"InvalidInput", {
@@ -46,31 +51,12 @@ declare const make: (engine: Engine, actors: ReadonlyArray<AnyActor>, sessions:
46
51
  }>]>;
47
52
  readonly isRetryable: Schema.Boolean;
48
53
  readonly retryAfter: Schema.optionalKey<Schema.Finite>;
49
- }>, never, never>>, Schema.Never, typeof Authorization, never>, any> | Rpc.ToHandlerFn<Rpc.Rpc<string, Schema.Struct<{
54
+ }>, never, never>, Schema.TaggedStruct<"ActionFailed", {
55
+ readonly error: Schema.Codec<Schema.Json, Schema.Json, never, never>;
56
+ readonly replayed: Schema.Boolean;
57
+ }>]>, typeof Authorization, never>, any> | Rpc.ToHandlerFn<Rpc.Rpc<string, Schema.Struct<{
50
58
  readonly id: Schema.Codec<Schema.Json, Schema.Json, never, never>;
51
- }>, import("effect/rpc/RpcSchema").Stream<Schema.Union<readonly [Schema.Struct<{
52
- readonly kind: Schema.Literal<"Frame">;
53
- readonly version: Schema.Finite;
54
- readonly value: Schema.Codec<Schema.Json, Schema.Json, never, never>;
55
- }>, Schema.Struct<{
56
- readonly kind: Schema.Literal<"Patch">;
57
- readonly version: Schema.Finite;
58
- readonly ops: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
59
- readonly op: Schema.Literal<"add">;
60
- readonly path: Schema.String;
61
- readonly value: Schema.Codec<Schema.Json, Schema.Json, never, never>;
62
- readonly description: Schema.optionalKey<Schema.String>;
63
- }>, Schema.Struct<{
64
- readonly op: Schema.Literal<"remove">;
65
- readonly path: Schema.String;
66
- readonly description: Schema.optionalKey<Schema.String>;
67
- }>, Schema.Struct<{
68
- readonly op: Schema.Literal<"replace">;
69
- readonly path: Schema.String;
70
- readonly value: Schema.Codec<Schema.Json, Schema.Json, never, never>;
71
- readonly description: Schema.optionalKey<Schema.String>;
72
- }>]>>;
73
- }>]>, Schema.decodeTo<typeof ActorError, Schema.TaggedStruct<"ActorError", {
59
+ }>, import("effect/rpc/RpcSchema").Stream<Schema.Codec<Schema.Json, Schema.Json, never, never>, Schema.decodeTo<typeof ActorError, Schema.TaggedStruct<"ActorError", {
74
60
  readonly reason: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Unauthorized", {
75
61
  readonly code: Schema.Literals<readonly ["missing_credentials", "invalid_credentials", "expired", "access_denied", "outcome_access_denied", "reauthorization_unavailable"]>;
76
62
  }>, Schema.TaggedStruct<"InvalidInput", {
@@ -102,7 +88,6 @@ declare const make: (engine: Engine, actors: ReadonlyArray<AnyActor>, sessions:
102
88
  readonly retryAfter: Schema.optionalKey<Schema.Finite>;
103
89
  }>, never, never>>, Schema.Never, typeof Authorization, never>, any> | Rpc.ToHandlerFn<Rpc.Rpc<string, Schema.Struct<{
104
90
  readonly id: Schema.Codec<Schema.Json, Schema.Json, never, never>;
105
- readonly params: Schema.Codec<Schema.Json, Schema.Json, never, never>;
106
91
  }>, import("effect/rpc/RpcSchema").Stream<Schema.Union<readonly [Schema.Struct<{
107
92
  readonly kind: Schema.Literal<"Frame">;
108
93
  readonly version: Schema.Finite;
@@ -157,10 +142,7 @@ declare const make: (engine: Engine, actors: ReadonlyArray<AnyActor>, sessions:
157
142
  readonly retryAfter: Schema.optionalKey<Schema.Finite>;
158
143
  }>, never, never>>, Schema.Never, typeof Authorization, never>, any> | Rpc.ToHandlerFn<Rpc.Rpc<string, Schema.Struct<{
159
144
  readonly id: Schema.Codec<Schema.Json, Schema.Json, never, never>;
160
- readonly from: Schema.optional<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
161
- readonly to: Schema.optional<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
162
- readonly limit: Schema.optional<Schema.Int>;
163
- readonly reverse: Schema.optional<Schema.Boolean>;
145
+ readonly params: Schema.Codec<Schema.Json, Schema.Json, never, never>;
164
146
  }>, import("effect/rpc/RpcSchema").Stream<Schema.Union<readonly [Schema.Struct<{
165
147
  readonly kind: Schema.Literal<"Frame">;
166
148
  readonly version: Schema.Finite;
@@ -215,8 +197,11 @@ declare const make: (engine: Engine, actors: ReadonlyArray<AnyActor>, sessions:
215
197
  readonly retryAfter: Schema.optionalKey<Schema.Finite>;
216
198
  }>, never, never>>, Schema.Never, typeof Authorization, never>, any> | Rpc.ToHandlerFn<Rpc.Rpc<string, Schema.Struct<{
217
199
  readonly id: Schema.Codec<Schema.Json, Schema.Json, never, never>;
218
- readonly presence: Schema.Codec<Schema.Json, Schema.Json, never, never>;
219
- }>, import("effect/rpc/RpcSchema").Stream<Schema.Literal<"joined">, Schema.decodeTo<typeof ActorError, Schema.TaggedStruct<"ActorError", {
200
+ readonly from: Schema.optional<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
201
+ readonly to: Schema.optional<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
202
+ readonly limit: Schema.optional<Schema.Int>;
203
+ readonly reverse: Schema.optional<Schema.Boolean>;
204
+ }>, Schema.$Array<Schema.Codec<Schema.Json, Schema.Json, never, never>>, Schema.decodeTo<typeof ActorError, Schema.TaggedStruct<"ActorError", {
220
205
  readonly reason: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Unauthorized", {
221
206
  readonly code: Schema.Literals<readonly ["missing_credentials", "invalid_credentials", "expired", "access_denied", "outcome_access_denied", "reauthorization_unavailable"]>;
222
207
  }>, Schema.TaggedStruct<"InvalidInput", {
@@ -246,14 +231,35 @@ declare const make: (engine: Engine, actors: ReadonlyArray<AnyActor>, sessions:
246
231
  }>]>;
247
232
  readonly isRetryable: Schema.Boolean;
248
233
  readonly retryAfter: Schema.optionalKey<Schema.Finite>;
249
- }>, never, never>>, Schema.Never, typeof Authorization, never>, any> | Rpc.ToHandlerFn<Rpc.Rpc<string, Schema.Struct<{
234
+ }>, never, never>, typeof Authorization, never>, any> | Rpc.ToHandlerFn<Rpc.Rpc<string, Schema.Struct<{
250
235
  readonly id: Schema.Codec<Schema.Json, Schema.Json, never, never>;
251
- readonly commandId: Schema.String;
252
- readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
253
- }>, Schema.Struct<{
236
+ readonly from: Schema.optional<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
237
+ readonly to: Schema.optional<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
238
+ readonly limit: Schema.optional<Schema.Int>;
239
+ readonly reverse: Schema.optional<Schema.Boolean>;
240
+ }>, import("effect/rpc/RpcSchema").Stream<Schema.Union<readonly [Schema.Struct<{
241
+ readonly kind: Schema.Literal<"Frame">;
242
+ readonly version: Schema.Finite;
254
243
  readonly value: Schema.Codec<Schema.Json, Schema.Json, never, never>;
255
- readonly replayed: Schema.Boolean;
256
- }>, Schema.Union<readonly [Schema.decodeTo<typeof ActorError, Schema.TaggedStruct<"ActorError", {
244
+ }>, Schema.Struct<{
245
+ readonly kind: Schema.Literal<"Patch">;
246
+ readonly version: Schema.Finite;
247
+ readonly ops: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
248
+ readonly op: Schema.Literal<"add">;
249
+ readonly path: Schema.String;
250
+ readonly value: Schema.Codec<Schema.Json, Schema.Json, never, never>;
251
+ readonly description: Schema.optionalKey<Schema.String>;
252
+ }>, Schema.Struct<{
253
+ readonly op: Schema.Literal<"remove">;
254
+ readonly path: Schema.String;
255
+ readonly description: Schema.optionalKey<Schema.String>;
256
+ }>, Schema.Struct<{
257
+ readonly op: Schema.Literal<"replace">;
258
+ readonly path: Schema.String;
259
+ readonly value: Schema.Codec<Schema.Json, Schema.Json, never, never>;
260
+ readonly description: Schema.optionalKey<Schema.String>;
261
+ }>]>>;
262
+ }>]>, Schema.decodeTo<typeof ActorError, Schema.TaggedStruct<"ActorError", {
257
263
  readonly reason: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Unauthorized", {
258
264
  readonly code: Schema.Literals<readonly ["missing_credentials", "invalid_credentials", "expired", "access_denied", "outcome_access_denied", "reauthorization_unavailable"]>;
259
265
  }>, Schema.TaggedStruct<"InvalidInput", {
@@ -283,16 +289,10 @@ declare const make: (engine: Engine, actors: ReadonlyArray<AnyActor>, sessions:
283
289
  }>]>;
284
290
  readonly isRetryable: Schema.Boolean;
285
291
  readonly retryAfter: Schema.optionalKey<Schema.Finite>;
286
- }>, never, never>, Schema.TaggedStruct<"ActionFailed", {
287
- readonly error: Schema.Codec<Schema.Json, Schema.Json, never, never>;
288
- readonly replayed: Schema.Boolean;
289
- }>]>, typeof Authorization, never>, any> | Rpc.ToHandlerFn<Rpc.Rpc<string, Schema.Struct<{
292
+ }>, never, never>>, Schema.Never, typeof Authorization, never>, any> | Rpc.ToHandlerFn<Rpc.Rpc<string, Schema.Struct<{
290
293
  readonly id: Schema.Codec<Schema.Json, Schema.Json, never, never>;
291
- readonly from: Schema.optional<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
292
- readonly to: Schema.optional<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
293
- readonly limit: Schema.optional<Schema.Int>;
294
- readonly reverse: Schema.optional<Schema.Boolean>;
295
- }>, Schema.$Array<Schema.Codec<Schema.Json, Schema.Json, never, never>>, Schema.decodeTo<typeof ActorError, Schema.TaggedStruct<"ActorError", {
294
+ readonly presence: Schema.Codec<Schema.Json, Schema.Json, never, never>;
295
+ }>, import("effect/rpc/RpcSchema").Stream<Schema.Literal<"joined">, Schema.decodeTo<typeof ActorError, Schema.TaggedStruct<"ActorError", {
296
296
  readonly reason: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Unauthorized", {
297
297
  readonly code: Schema.Literals<readonly ["missing_credentials", "invalid_credentials", "expired", "access_denied", "outcome_access_denied", "reauthorization_unavailable"]>;
298
298
  }>, Schema.TaggedStruct<"InvalidInput", {
@@ -322,7 +322,7 @@ declare const make: (engine: Engine, actors: ReadonlyArray<AnyActor>, sessions:
322
322
  }>]>;
323
323
  readonly isRetryable: Schema.Boolean;
324
324
  readonly retryAfter: Schema.optionalKey<Schema.Finite>;
325
- }>, never, never>, typeof Authorization, never>, any> | Rpc.ToHandlerFn<Rpc.Rpc<string, Schema.Struct<{
325
+ }>, never, never>>, Schema.Never, typeof Authorization, never>, any> | Rpc.ToHandlerFn<Rpc.Rpc<string, Schema.Struct<{
326
326
  readonly where: Schema.$Record<Schema.String, Schema.Struct<{
327
327
  readonly eq: Schema.optionalKey<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
328
328
  readonly gt: Schema.optionalKey<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
@@ -18,6 +18,17 @@ export class ForkRefused extends Schema.TaggedError()("ForkRefused", {
18
18
  }) {
19
19
  }
20
20
  const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
21
+ /**
22
+ * A position no retained checkpoint can serve precedes the oldest one. When the newest position
23
+ * that checkpoint covers is older than the fork's retention cutoff, every position through it, the
24
+ * requested one included, is outside retention and its log prefix may already be trimmed, so the
25
+ * fork is refused for retention without reading the log. That time is the later of the shard's last
26
+ * action and the latest position any actor holds, because a fork installed into a shard records a
27
+ * position without advancing the shard's last action. The checkpoint's own contents decide this, on
28
+ * the same clock as the admission check, because which segments survive says nothing about
29
+ * retention once in-window segments are lost. Otherwise the log is read from its start, and a
30
+ * segment missing there is corruption.
31
+ */
21
32
  const forkOnce = (target, options, source = target.store) => Effect.gen(function* () {
22
33
  const { actor, from, at, grant } = options;
23
34
  const tenant = options.tenant ?? "default";
@@ -45,8 +56,10 @@ const forkOnce = (target, options, source = target.store) => Effect.gen(function
45
56
  const cache = new PageCache();
46
57
  const available = yield* source.history.checkpoints(shard);
47
58
  let base;
59
+ let oldestAt;
48
60
  for (const retained of available) {
49
61
  const decoded = Result.getOrThrow(Checkpoints.decode(yield* PagedCheckpoints.read(retained.statePages[0], source)));
62
+ oldestAt ??= decoded.actors.reduce((latest, candidate) => Math.max(latest, candidate.position?.at ?? 0), decoded.lastAt);
50
63
  const snapshot = decoded.actors.find((candidate) => candidate.actor === actor.name && same(candidate.key, from));
51
64
  const suitable = "offset" in at
52
65
  ? decoded.offset < at.offset
@@ -56,6 +69,8 @@ const forkOnce = (target, options, source = target.store) => Effect.gen(function
56
69
  if (suitable && (base === undefined || decoded.offset > base.offset))
57
70
  base = decoded;
58
71
  }
72
+ if (base === undefined && oldestAt !== undefined && oldestAt < cutoff)
73
+ return yield* ForkRefused.make({ reason: "retention" });
59
74
  const image = base === undefined ? Durable.empty(shard) : Checkpoints.restore(catalog, base, cache);
60
75
  const start = base?.offset ?? 0;
61
76
  const initial = image.actors.get(Durable.addressOf(actor.name, from));
@@ -38,6 +38,12 @@ export interface StoredSegment extends Segment {
38
38
  /** A retained checkpoint and its provider's last-modified time in epoch milliseconds. */
39
39
  export interface HistoryCheckpoint extends Manifest {
40
40
  readonly modified: number;
41
+ /**
42
+ * The checkpoint's offset precedes the oldest stored segment written within retention, or no
43
+ * stored segment was written within it. Lost in-window segments move that boundary later, so
44
+ * this does not prove the checkpoint's positions are outside retention.
45
+ */
46
+ readonly expired: boolean;
41
47
  }
42
48
  /**
43
49
  * The bucket log protocol. A segment is created once at `head + 1`; the
package/dist/store/log.js CHANGED
@@ -248,7 +248,11 @@ export const bucketLog = (settings) => {
248
248
  key,
249
249
  reason: "checkpoint offset differs from its key",
250
250
  });
251
- retained.push({ ...manifest, modified });
251
+ retained.push({
252
+ ...manifest,
253
+ modified,
254
+ expired: window.first === undefined || offset < window.first,
255
+ });
252
256
  }
253
257
  return retained.reverse();
254
258
  }).pipe(Effect.catchTag("StorageUnavailable", (error) => Effect.fail(unavailable(error))));
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@rikalabs/akter","version":"0.2.1","files":["dist","CHANGELOG.md","README.md","LICENSE","NOTICE"],"dependencies":{"@aws-sdk/client-s3":"3.1148.0","@effect/platform-node-shared":"4.0.3"},"peerDependencies":{"@effect/platform-bun":"^4.0.3","@effect/platform-node":"^4.0.3","effect":"^4.0.3"},"publishConfig":{"access":"public"},"description":"The framework for durable, stateful backends that power realtime apps, background work, and agents.","keywords":["actor-model","actors","agents","background-jobs","bun","durable","effect","effect-ts","realtime","stateful","transactions","workflows"],"homepage":"https://github.com/Rika-Labs/akter#readme","bugs":"https://github.com/Rika-Labs/akter/issues","license":"Apache-2.0","author":"Rika Labs","repository":{"type":"git","url":"git+https://github.com/Rika-Labs/akter.git","directory":"packages/akter"},"type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./server":{"types":"./dist/server.d.ts","default":"./dist/server.js"},"./client":{"types":"./dist/client/index.d.ts","default":"./dist/client/index.js"},"./testing":{"types":"./dist/testing/index.d.ts","default":"./dist/testing/index.js"},"./package.json":"./package.json"},"peerDependenciesMeta":{"@effect/platform-bun":{"optional":true},"@effect/platform-node":{"optional":true}},"engines":{"bun":">=1.4.2","node":">=24.0.0"}}
1
+ {"name":"@rikalabs/akter","version":"0.2.2","files":["dist","CHANGELOG.md","README.md","LICENSE","NOTICE"],"dependencies":{"@aws-sdk/client-s3":"3.1148.0","@effect/platform-node-shared":"4.0.3"},"peerDependencies":{"@effect/platform-bun":"^4.0.3","@effect/platform-node":"^4.0.3","effect":"^4.0.3"},"publishConfig":{"access":"public"},"description":"The framework for durable, stateful backends that power realtime apps, background work, and agents.","keywords":["actor-model","actors","agents","background-jobs","bun","durable","effect","effect-ts","realtime","stateful","transactions","workflows"],"homepage":"https://github.com/Rika-Labs/akter#readme","bugs":"https://github.com/Rika-Labs/akter/issues","license":"Apache-2.0","author":"Rika Labs","repository":{"type":"git","url":"git+https://github.com/Rika-Labs/akter.git","directory":"packages/akter"},"type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./server":{"types":"./dist/server.d.ts","default":"./dist/server.js"},"./client":{"types":"./dist/client/index.d.ts","default":"./dist/client/index.js"},"./testing":{"types":"./dist/testing/index.d.ts","default":"./dist/testing/index.js"},"./package.json":"./package.json"},"peerDependenciesMeta":{"@effect/platform-bun":{"optional":true},"@effect/platform-node":{"optional":true}},"engines":{"bun":">=1.4.2","node":">=24.0.0"}}