@streamsy/storage 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bun.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { r as SqlStorageOptions } from "./storage-CrMmTyyG.js";
1
+ import { r as SqlStorageOptions } from "./storage-Cb_kOO_g.js";
2
2
  import { Layer } from "effect";
3
3
  import { StorageFault, Streamsy } from "@streamsy/core";
4
4
  import { Protocol } from "@streamsy/core/protocol";
@@ -16,5 +16,4 @@ declare const storageLayer: (options: BunStorageOptions) => Layer.Layer<import("
16
16
  /** Complete persistent protocol Layer for an HttpRouter application. */
17
17
  declare const layerProtocol: (options: Options) => Layer.Layer<import("@streamsy/core").CommitBoundary | import("@streamsy/core/internal").OwnerRecords | import("effect/reactivity/Reactivity").Reactivity | import("effect/sql/SqlClient").SqlClient | import("@streamsy/core").Storage | import("@streamsy/core/protocol").StreamsReader | import("@streamsy/core/protocol").StreamsWriter, StorageFault, never>;
18
18
  declare function layer<const Keys extends ReadonlyArray<Streamsy.Any>>(group: Streamsy.Group<Keys>, options: Options): Layer.Layer<Layer.Success<ReturnType<typeof storageLayer>> | Streamsy.Host<Streamsy.Group<Keys>>, Layer.Error<ReturnType<typeof storageLayer>>>;
19
- //#endregion
20
19
  export { bun_storage_d_exports as BunStorage };
package/dist/bun.js CHANGED
@@ -1,13 +1,13 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { a as storageLayer$1, d as retryWithPolicy, f as transactionRetryPolicy, o as inspectFormat, s as newerVersionFault, u as sharedSqlClientLayer } from "./storage-CpnFxwO5.js";
3
- import { Cause, Effect, FileSystem, Layer, Option, Path, Result, Schema } from "effect";
1
+ import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.js";
2
+ import { a as storageLayer$1, d as retryWithPolicy, f as transactionRetryPolicy, o as inspectFormat, s as newerVersionFault, u as sharedSqlClientLayer } from "./storage-DFCZf1UK.js";
3
+ import { Array, Cause, Effect, FileSystem, Layer, Option, Path, Result, Schema } from "effect";
4
4
  import { StorageFault } from "@streamsy/core";
5
5
  import { Protocol } from "@streamsy/core/protocol";
6
6
  import * as BunFileSystem from "@effect/platform-bun/BunFileSystem";
7
7
  import * as BunPath from "@effect/platform-bun/BunPath";
8
8
  import { SqliteClient } from "@effect/sql-sqlite-bun";
9
9
  import { Database } from "bun:sqlite";
10
- //#region src/sqlite-file.ts
10
+ // src/sqlite-file.ts
11
11
  const openFault$1 = (message) => new StorageFault({
12
12
  operation: "migration.open",
13
13
  message,
@@ -16,7 +16,7 @@ const openFault$1 = (message) => new StorageFault({
16
16
  const decodeTableNames = Schema.decodeUnknownSync(Schema.Array(Schema.Struct({ name: Schema.String })));
17
17
  const decodeVersions = Schema.decodeUnknownSync(Schema.Array(Schema.Struct({ version: Schema.NullOr(Schema.Finite) })));
18
18
  const readFormat = (reader) => {
19
- const names = new Set(decodeTableNames(reader.all("SELECT name FROM sqlite_master WHERE type='table'")).map(({ name }) => name));
19
+ const names = new Set(Array.map(decodeTableNames(reader.all("SELECT name FROM sqlite_master WHERE type='table'")), ({ name }) => name));
20
20
  return {
21
21
  names,
22
22
  version: names.has("streamsy_schema") ? decodeVersions(reader.all(`SELECT MAX(version) version FROM streamsy_schema`)).at(0)?.version ?? 0 : 0
@@ -54,8 +54,7 @@ const prepare = (driver, options, retry) => Effect.gen(function* () {
54
54
  create: options.create ?? true
55
55
  }, ["PRAGMA busy_timeout=0", "PRAGMA journal_mode=WAL"]));
56
56
  });
57
- //#endregion
58
- //#region src/bun-storage.ts
57
+ // src/bun-storage.ts
59
58
  var bun_storage_exports = /* @__PURE__ */ __exportAll({
60
59
  layer: () => layer,
61
60
  layerProtocol: () => layerProtocol
@@ -105,5 +104,4 @@ function layer(group, options) {
105
104
  const protocol = Protocol.layer(options).pipe(Layer.provideMerge(storage));
106
105
  return Protocol.bind(protocol, group).pipe(Layer.provideMerge(storage));
107
106
  }
108
- //#endregion
109
107
  export { bun_storage_exports as BunStorage };
@@ -1,4 +1,4 @@
1
- import { r as SqlStorageOptions } from "./storage-CrMmTyyG.js";
1
+ import { r as SqlStorageOptions } from "./storage-Cb_kOO_g.js";
2
2
  import { Layer } from "effect";
3
3
  import { Streamsy } from "@streamsy/core";
4
4
  import { Protocol } from "@streamsy/core/protocol";
@@ -20,5 +20,4 @@ declare const storageLayer: (options: DurableObjectStorageOptions) => Layer.Laye
20
20
  /** Protocol over the official Durable Object SQLite layer. */
21
21
  declare const layerProtocol: (options: Options) => Layer.Layer<import("@streamsy/core").CommitBoundary | import("@streamsy/core/internal").OwnerRecords | import("effect/reactivity/Reactivity").Reactivity | import("effect/sql/SqlClient").SqlClient | import("@streamsy/core").Storage | import("@streamsy/core/protocol").StreamsReader | import("@streamsy/core/protocol").StreamsWriter, import("@streamsy/core").StorageFault, never>;
22
22
  declare function layer<const Keys extends ReadonlyArray<Streamsy.Any>>(group: Streamsy.Group<Keys>, options: Options): Layer.Layer<Layer.Success<ReturnType<typeof storageLayer>> | Streamsy.Host<Streamsy.Group<Keys>>, Layer.Error<ReturnType<typeof storageLayer>>>;
23
- //#endregion
24
23
  export { durable_object_storage_d_exports as DurableObjectStorage };
@@ -1,10 +1,10 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { i as layerWithDurableObjectTransactions, u as sharedSqlClientLayer } from "./storage-CpnFxwO5.js";
1
+ import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.js";
2
+ import { i as layerWithDurableObjectTransactions, u as sharedSqlClientLayer } from "./storage-DFCZf1UK.js";
3
3
  import { Layer } from "effect";
4
4
  import "@streamsy/core";
5
5
  import { Protocol } from "@streamsy/core/protocol";
6
6
  import { SqliteClient } from "@effect/sql-sqlite-do";
7
- //#region src/durable-object-storage.ts
7
+ // src/durable-object-storage.ts
8
8
  var durable_object_storage_exports = /* @__PURE__ */ __exportAll({
9
9
  DEFAULT_LONG_POLL_TIMEOUT_MS: () => DEFAULT_LONG_POLL_TIMEOUT_MS,
10
10
  layer: () => layer,
@@ -29,5 +29,4 @@ function layer(group, options) {
29
29
  }).pipe(Layer.provideMerge(storage));
30
30
  return Protocol.bind(protocol, group).pipe(Layer.provideMerge(storage));
31
31
  }
32
- //#endregion
33
32
  export { durable_object_storage_exports as DurableObjectStorage };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- import { a as layerProtocol, c as DEFAULT_TRANSACTION_RETRY_DELAY_MS, i as layer, n as Options, o as clientLayer, s as DEFAULT_TRANSACTION_RETRY_ATTEMPTS, t as DEFAULT_REPAIR_INTERVAL_MS } from "./storage-CrMmTyyG.js";
1
+ import { a as layerProtocol, c as DEFAULT_TRANSACTION_RETRY_DELAY_MS, i as layer, n as Options, o as clientLayer, s as DEFAULT_TRANSACTION_RETRY_ATTEMPTS, t as DEFAULT_REPAIR_INTERVAL_MS } from "./storage-Cb_kOO_g.js";
2
2
  declare namespace sql_storage_d_exports {
3
3
  export { DEFAULT_REPAIR_INTERVAL_MS, DEFAULT_TRANSACTION_RETRY_ATTEMPTS, DEFAULT_TRANSACTION_RETRY_DELAY_MS, Options, clientLayer, layer, layerProtocol };
4
4
  }
5
- //#endregion
6
5
  export { sql_storage_d_exports as SqlStorage };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { l as clientLayer, n as layer, r as layerProtocol, t as DEFAULT_REPAIR_INTERVAL_MS } from "./storage-CpnFxwO5.js";
3
- //#region src/sql-storage.ts
1
+ import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.js";
2
+ import { l as clientLayer, n as layer, r as layerProtocol, t as DEFAULT_REPAIR_INTERVAL_MS } from "./storage-DFCZf1UK.js";
3
+ // src/sql-storage.ts
4
4
  var sql_storage_exports = /* @__PURE__ */ __exportAll({
5
5
  DEFAULT_REPAIR_INTERVAL_MS: () => DEFAULT_REPAIR_INTERVAL_MS,
6
6
  DEFAULT_TRANSACTION_RETRY_ATTEMPTS: () => 16,
@@ -9,5 +9,4 @@ var sql_storage_exports = /* @__PURE__ */ __exportAll({
9
9
  layer: () => layer,
10
10
  layerProtocol: () => layerProtocol
11
11
  });
12
- //#endregion
13
12
  export { sql_storage_exports as SqlStorage };
@@ -1,4 +1,4 @@
1
- //#region \0rolldown/runtime.js
1
+ // \0rolldown/runtime.js
2
2
  var __defProp = Object.defineProperty;
3
3
  var __exportAll = (all, no_symbols) => {
4
4
  let target = {};
@@ -9,5 +9,4 @@ var __exportAll = (all, no_symbols) => {
9
9
  if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
10
  return target;
11
11
  };
12
- //#endregion
13
12
  export { __exportAll as t };
@@ -1,15 +1,11 @@
1
- import { Context, Effect, Layer, Queue, Scope, Stream } from "effect";
2
- import * as Reactivity from "effect/reactivity/Reactivity";
3
- import * as SqlClient from "effect/sql/SqlClient";
4
- import "effect/sql/SqlError";
1
+ import { Context, Effect, Layer, PubSub, Queue, Scope, Stream } from "effect";
2
+ import { Reactivity } from "effect/reactivity";
3
+ import { SqlClient, SqlError } from "effect/sql";
5
4
  import { OwnerRecords } from "@streamsy/core/internal";
6
- import { CommitBoundary, Storage, StorageFault, Streamsy } from "@streamsy/core";
5
+ import { CommitBoundary, Storage, StorageFault, StreamId, Streamsy } from "@streamsy/core";
7
6
  import { Protocol } from "@streamsy/core/protocol";
8
- //#region src/transaction-retry.d.ts
9
7
  declare const DEFAULT_TRANSACTION_RETRY_ATTEMPTS = 16;
10
8
  declare const DEFAULT_TRANSACTION_RETRY_DELAY_MS = 25;
11
- //#endregion
12
- //#region src/boundary.d.ts
13
9
  /**
14
10
  * Build a `SqlClient` and provide it with the `Reactivity` it captured, so the
15
11
  * client and the storage boundary share one change graph.
@@ -21,8 +17,6 @@ declare const DEFAULT_TRANSACTION_RETRY_DELAY_MS = 25;
21
17
  * ```
22
18
  */
23
19
  declare const clientLayer: <A extends SqlClient.SqlClient, E>(makeClient: Effect.Effect<A, E, Scope.Scope | Reactivity.Reactivity>) => Layer.Layer<SqlClient.SqlClient | Reactivity.Reactivity, E>;
24
- //#endregion
25
- //#region src/storage.d.ts
26
20
  declare const DEFAULT_REPAIR_INTERVAL_MS = 1000;
27
21
  interface SqlStorageOptions {
28
22
  /** Cross-process and post-commit-interruption staleness bound. */
@@ -39,5 +33,4 @@ interface Options extends SqlStorageOptions, Protocol.ProtocolOptions {}
39
33
  /** Protocol over SQL storage. */
40
34
  declare const layerProtocol: (options?: Options) => Layer.Layer<CommitBoundary | OwnerRecords | Storage | import("@streamsy/core/protocol").StreamsReader | import("@streamsy/core/protocol").StreamsWriter, StorageFault, Reactivity.Reactivity | SqlClient.SqlClient>;
41
35
  declare function layer<const Keys extends ReadonlyArray<Streamsy.Any>>(group: Streamsy.Group<Keys>, options?: Options): Layer.Layer<Layer.Success<ReturnType<typeof storageLayer>> | Streamsy.Host<Streamsy.Group<Keys>>, Layer.Error<ReturnType<typeof storageLayer>>, Layer.Services<ReturnType<typeof storageLayer>>>;
42
- //#endregion
43
36
  export { layerProtocol as a, DEFAULT_TRANSACTION_RETRY_DELAY_MS as c, layer as i, Options as n, clientLayer as o, SqlStorageOptions as r, DEFAULT_TRANSACTION_RETRY_ATTEMPTS as s, DEFAULT_REPAIR_INTERVAL_MS as t };
@@ -1,8 +1,6 @@
1
- import { Clock, Context, Deferred, Effect, Fiber, Layer, Option, Predicate, Queue, Schedule, Schema, Stream } from "effect";
2
- import * as Scheduler from "effect/Scheduler";
3
- import * as Reactivity from "effect/reactivity/Reactivity";
4
- import * as SqlClient from "effect/sql/SqlClient";
5
- import { isSqlError } from "effect/sql/SqlError";
1
+ import { Array, Clock, Context, Deferred, Effect, Fiber, Layer, Option, Order, Predicate, PubSub, Queue, Schedule, Scheduler, Schema, Stream } from "effect";
2
+ import { Reactivity } from "effect/reactivity";
3
+ import { SqlClient, SqlError } from "effect/sql";
6
4
  import { OwnerRecords, attachOwner, ownerOf } from "@streamsy/core/internal";
7
5
  import { CommitBoundary, MutationRejected, Mutations, Offset, OperationResults, Storage, StorageFault, StreamId, ZERO_OFFSET } from "@streamsy/core";
8
6
  import { Protocol } from "@streamsy/core/protocol";
@@ -22,8 +20,7 @@ const retryWithPolicy = (transaction, policy, isRetryable) => transaction.pipe(E
22
20
  schedule: Schedule.spaced(policy.delayMs),
23
21
  while: isRetryable
24
22
  }));
25
- //#endregion
26
- //#region src/boundary.ts
23
+ // src/boundary.ts
27
24
  var PendingInvalidations = class extends Context.Service()("@streamsy/storage/PendingInvalidations") {};
28
25
  const noop = () => {};
29
26
  var BoundaryRuntimeService = class extends Context.Service()("@streamsy/storage/BoundaryRuntime") {};
@@ -44,6 +41,7 @@ const makeBoundary = (repairIntervalMs, transactionRetry, probe, transactionMaxO
44
41
  const sql = yield* SqlClient.SqlClient;
45
42
  const reactivity = yield* Reactivity.Reactivity;
46
43
  const subscriptions = /* @__PURE__ */ new Set();
44
+ const feed = yield* Effect.acquireRelease(PubSub.unbounded(), PubSub.shutdown);
47
45
  let closed = false;
48
46
  yield* Effect.addFinalizer(() => Effect.gen(function* () {
49
47
  const active = yield* Effect.sync(() => {
@@ -55,17 +53,20 @@ const makeBoundary = (repairIntervalMs, transactionRetry, probe, transactionMaxO
55
53
  }));
56
54
  const ownedTransaction = (body) => Effect.suspend(() => {
57
55
  if (probe !== void 0) probe.transactionAttempts += 1;
58
- const pending = /* @__PURE__ */ new Set();
56
+ const pending = {
57
+ keys: /* @__PURE__ */ new Set(),
58
+ ids: /* @__PURE__ */ new Set()
59
+ };
59
60
  let bodyCompleted = false;
60
61
  const transactionBody = body.pipe(Effect.provideService(PendingInvalidations, pending), Effect.tap(() => Effect.sync(() => {
61
62
  bodyCompleted = true;
62
63
  })));
63
- const transaction = sql.withTransaction(transactionBody).pipe(Effect.catchIf((error) => bodyCompleted && isSqlError(error), (error) => Effect.die(error)));
64
+ const transaction = sql.withTransaction(transactionBody).pipe(Effect.catchIf((error) => bodyCompleted && SqlError.isSqlError(error), (error) => Effect.die(error)));
64
65
  return (transactionMaxOpsBeforeYield === void 0 ? transaction : transaction.pipe(Effect.provideService(Scheduler.MaxOpsBeforeYield, transactionMaxOpsBeforeYield))).pipe(Effect.tap(() => {
65
- if (pending.size === 0) return Effect.void;
66
+ if (pending.keys.size === 0) return Effect.void;
66
67
  if (probe !== void 0) probe.invalidations += 1;
67
- return reactivity.invalidate([...pending]);
68
- }));
68
+ return reactivity.invalidate([...pending.keys]);
69
+ }), Effect.tap(() => pending.ids.size === 0 ? Effect.void : PubSub.publish(feed, [...pending.ids])));
69
70
  });
70
71
  const withTransaction = (body) => Effect.flatMap(Effect.serviceOption(PendingInvalidations), (owned) => {
71
72
  if (Option.isSome(owned)) return body;
@@ -74,15 +75,16 @@ const makeBoundary = (repairIntervalMs, transactionRetry, probe, transactionMaxO
74
75
  return ownedTransaction(body);
75
76
  });
76
77
  });
77
- const mutation = ({ keys, effect, retryable }) => Effect.flatMap(Effect.serviceOption(sql.transactionService), (ambient) => Effect.flatMap(Effect.serviceOption(PendingInvalidations), (pendingBefore) => {
78
+ const mutation = ({ keys, ids, effect, retryable }) => Effect.flatMap(Effect.serviceOption(sql.transactionService), (ambient) => Effect.flatMap(Effect.serviceOption(PendingInvalidations), (pendingBefore) => {
78
79
  if (Option.isSome(ambient) && Option.isNone(pendingBefore)) return Effect.die(/* @__PURE__ */ new Error("Ambient SQL transaction is not owned by CommitBoundary"));
79
80
  const run = Effect.tap(effect, () => Effect.flatMap(Effect.serviceOption(PendingInvalidations), (pending) => {
80
81
  if (Option.isNone(pending)) return Effect.die(/* @__PURE__ */ new Error("CommitBoundary owner context disappeared"));
81
82
  return Effect.sync(() => {
82
- for (const key of keys) pending.value.add(key);
83
+ for (const key of keys) pending.value.keys.add(key);
84
+ for (const id of ids) pending.value.ids.add(id);
83
85
  });
84
86
  }));
85
- return Option.isSome(ambient) ? run : retryWithPolicy(ownedTransaction(run), transactionRetry, (error) => isSqlError(error) ? error.isRetryable : retryable(error));
87
+ return Option.isSome(ambient) ? run : retryWithPolicy(ownedTransaction(run), transactionRetry, (error) => SqlError.isSqlError(error) ? error.isRetryable : retryable(error));
86
88
  })).pipe(Effect.onExit(() => Effect.sync(() => {
87
89
  if (probe !== void 0) probe.completedMutations += 1;
88
90
  })));
@@ -137,12 +139,12 @@ const makeBoundary = (repairIntervalMs, transactionRetry, probe, transactionMaxO
137
139
  return {
138
140
  withTransaction,
139
141
  mutation,
140
- changes
142
+ changes,
143
+ committed: PubSub.subscribe(feed)
141
144
  };
142
145
  });
143
146
  const boundaryLayer = (repairIntervalMs, transactionRetry, probe, transactionMaxOpsBeforeYield) => Layer.effect(BoundaryRuntimeService, makeBoundary(repairIntervalMs, transactionRetry, probe, transactionMaxOpsBeforeYield));
144
- //#endregion
145
- //#region src/dialect.ts
147
+ // src/dialect.ts
146
148
  /** The client's dialect. Other dialects fail with a non-retryable fault. */
147
149
  const storageDialect = (sql) => sql.onDialectOrElse({
148
150
  sqlite: () => Effect.succeed("sqlite"),
@@ -154,8 +156,7 @@ const storageDialect = (sql) => sql.onDialectOrElse({
154
156
  retryable: false
155
157
  }))
156
158
  });
157
- //#endregion
158
- //#region src/schema.ts
159
+ // src/schema.ts
159
160
  const STORAGE_VERSION_TABLE = "streamsy_schema";
160
161
  /** Version tables of earlier formats. A database that holds one must not be migrated. */
161
162
  const LEGACY_VERSION_TABLES = ["streamsy_schema_version", "streamsy_storage_schema_version"];
@@ -258,8 +259,7 @@ const STORAGE_MIGRATIONS = [{
258
259
  ) ENGINE=InnoDB`
259
260
  ]
260
261
  }];
261
- //#endregion
262
- //#region src/migrations.ts
262
+ // src/migrations.ts
263
263
  /** Fixed `pg_advisory_xact_lock` key: "streamsy" in ASCII, read as a 64-bit integer. */
264
264
  const PG_MIGRATION_LOCK_KEY = "8319400191411975033";
265
265
  /** Named lock that serialises MySQL migrations, whose DDL cannot share one transaction. */
@@ -275,7 +275,7 @@ const sqlAttempt = (operation, effect) => effect.pipe(Effect.mapError((error) =>
275
275
  /** Return whether the database already holds this format, or fail on an older format. */
276
276
  const inspectFormat = (tables) => {
277
277
  const ownsFormat = tables.has(STORAGE_VERSION_TABLE);
278
- return LEGACY_VERSION_TABLES.some((name) => tables.has(name)) || !ownsFormat && STORAGE_TABLES.some((name) => tables.has(name)) ? Effect.fail(new StorageFault({
278
+ return Array.some(LEGACY_VERSION_TABLES, (name) => tables.has(name)) || !ownsFormat && Array.some(STORAGE_TABLES, (name) => tables.has(name)) ? Effect.fail(new StorageFault({
279
279
  operation: "migration.open",
280
280
  message: "Unsupported older Streamsy storage format; use a fresh database path (the database was not modified)",
281
281
  retryable: false
@@ -292,7 +292,7 @@ const listTables = (sql, dialect) => sqlAttempt("migration.inspect", {
292
292
  WHERE table_schema = current_schema()`,
293
293
  mysql: () => sql`SELECT table_name AS name FROM information_schema.tables
294
294
  WHERE table_schema = DATABASE()`
295
- }[dialect]()).pipe(Effect.map((rows) => new Set(rows.map(({ name }) => name))));
295
+ }[dialect]()).pipe(Effect.map((rows) => new Set(Array.map(rows, ({ name }) => name))));
296
296
  const migrateVersions = (sql, dialect, hooks) => Effect.gen(function* () {
297
297
  if (!(yield* inspectFormat(yield* listTables(sql, dialect)))) yield* sqlAttempt("migration.create-version-table", sql.unsafe(STORAGE_VERSION_TABLE_DDL[dialect]).pipe(Effect.asVoid));
298
298
  const current = yield* sqlAttempt("migration.read-version", sql`SELECT MAX(version) AS version FROM ${sql(STORAGE_VERSION_TABLE)}`.pipe(Effect.map((rows) => Number(rows[0]?.version ?? 0))));
@@ -332,9 +332,8 @@ const migrate = (sql, hooks = {}, transactionRetry = transactionRetryPolicy({}))
332
332
  hooks.onTransactionAttempt?.();
333
333
  if (dialect === "mysql") return migrateMysql(sql, hooks);
334
334
  return sql.withTransaction(dialect === "pg" ? sqlAttempt("migration.lock", sql`SELECT pg_advisory_xact_lock(${sql.literal(PG_MIGRATION_LOCK_KEY)})`).pipe(Effect.andThen(migrateVersions(sql, dialect, hooks))) : migrateVersions(sql, dialect, hooks));
335
- }), transactionRetry, (error) => isSqlError(error) ? error.isRetryable : error.retryable).pipe(Effect.catchIf(isSqlError, (error) => Effect.fail(fault("migration.transaction", error)))));
336
- //#endregion
337
- //#region src/storage.ts
335
+ }), transactionRetry, (error) => SqlError.isSqlError(error) ? error.isRetryable : error.retryable).pipe(Effect.catchIf(SqlError.isSqlError, (error) => Effect.fail(fault("migration.transaction", error)))));
336
+ // src/storage.ts
338
337
  const DEFAULT_REPAIR_INTERVAL_MS = 1e3;
339
338
  const STORAGE_KEY = "streamsy:storage";
340
339
  const RecordRow = Schema.Struct({
@@ -467,7 +466,7 @@ const oversizedKey = (mutation) => {
467
466
  * Sorted ids give every writer one lock order. SQLite already has one writer.
468
467
  */
469
468
  const lockStreams = (sql, ids) => {
470
- const sorted = [...new Set(ids)].toSorted();
469
+ const sorted = Array.sort(new Set(ids), Order.String);
471
470
  return sql.onDialectOrElse({
472
471
  sqlite: () => Effect.void,
473
472
  orElse: () => execute("mutate.lock", sql`SELECT stream_id FROM streamsy_streams WHERE stream_id IN ${sql.in(sorted)}
@@ -493,7 +492,7 @@ const lockingRead = (sql, lock) => sql.onDialectOrElse({
493
492
  * mutation. Outside a transaction the read stays plain and takes no lock.
494
493
  */
495
494
  const planningLock = (sql) => Effect.map(Effect.serviceOption(sql.transactionService), (open) => Option.isSome(open) ? "update" : void 0);
496
- const lockIds = (mutation) => mutation.operations.flatMap((operation) => Predicate.isTagged(operation, "Create") ? operation.forkSource === void 0 ? [operation.record.id] : [operation.record.id, operation.forkSource.id] : [operation.streamId]);
495
+ const lockIds = (mutation) => Array.flatMap(mutation.operations, (operation) => Predicate.isTagged(operation, "Create") ? operation.forkSource === void 0 ? [operation.record.id] : [operation.record.id, operation.forkSource.id] : [operation.streamId]);
497
496
  const recordColumns = (sql) => sql.literal("stream_id,content_type,ttl_seconds,expires_at,created_at,current_offset,last_seq,closed,closed_at,forked_from,fork_offset,fork_sub_offset,soft_deleted,expires_at_ms");
498
497
  const readRecord = (sql, id, lock) => attempt("record", sql`SELECT ${recordColumns(sql)} FROM streamsy_streams WHERE stream_id = ${id}${lockingRead(sql, lock)}`).pipe(Effect.flatMap((rows) => {
499
498
  const row = rows[0];
@@ -689,7 +688,7 @@ const readMessages = (sql, id, window) => Effect.gen(function* () {
689
688
  until = record.value.lifecycle.forkOffset;
690
689
  }
691
690
  const messages = [];
692
- for (const segment of chain.toReversed()) {
691
+ for (const segment of Array.reverse(chain)) {
693
692
  if (remaining === 0) break;
694
693
  const upper = segment.until === void 0 ? window.until : window.until === void 0 || segment.until < window.until ? segment.until : window.until;
695
694
  const predicates = [sql`stream_id = ${segment.id}`];
@@ -724,20 +723,22 @@ const makeStorage = (sql, boundary) => Storage.of({
724
723
  messages: Effect.fn("SqlStorage.messages")((id, window) => readMessages(sql, id, window)),
725
724
  producer: Effect.fn("SqlStorage.producer")((id, producerId) => Effect.flatMap(planningLock(sql), (lock) => readProducer(sql, "producer", id, producerId, lock))),
726
725
  mutate: Effect.fn("SqlStorage.mutate")((mutation) => {
727
- const ids = mutation.operations.map((operation) => Predicate.isTagged(operation, "Create") ? operation.record.id : operation.streamId);
726
+ const ids = Array.map(mutation.operations, (operation) => Predicate.isTagged(operation, "Create") ? operation.record.id : operation.streamId);
728
727
  if (ids.length === 0 || new Set(ids).size !== ids.length) return Effect.die(/* @__PURE__ */ new Error("Mutation requires distinct streams within atomicScope"));
729
728
  const oversized = oversizedKey(mutation);
730
729
  if (Option.isSome(oversized)) return Effect.fail(oversized.value);
731
730
  return boundary.mutation({
732
731
  keys: [STORAGE_KEY],
732
+ ids,
733
733
  effect: applyMutation(sql, mutation),
734
734
  retryable: (error) => Predicate.isTagged(error, "StorageFault") && error.retryable
735
- }).pipe(Effect.catchIf(isSqlError, (error) => Effect.fail(sqlFault("mutate.transaction", error))));
735
+ }).pipe(Effect.catchIf(SqlError.isSqlError, (error) => Effect.fail(sqlFault("mutate.transaction", error))));
736
736
  }),
737
737
  changes: (id) => boundary.changes({
738
738
  keys: [STORAGE_KEY],
739
739
  read: snapshot(sql, id)
740
740
  }),
741
+ committed: boundary.committed,
741
742
  nextExpiry: attempt("nextExpiry", sql`SELECT expires_at_ms AS at, stream_id FROM streamsy_streams
742
743
  WHERE expires_at_ms IS NOT NULL AND soft_deleted = 0
743
744
  ORDER BY expires_at_ms, stream_id LIMIT 1`).pipe(Effect.flatMap((rows) => rows[0] === void 0 ? Effect.succeed(Option.none()) : decodeExpiryRow(rows[0]).pipe(Effect.map(({ at, stream_id: streamId }) => Option.some({
@@ -834,5 +835,4 @@ function layer(group, options = {}) {
834
835
  const protocol = Protocol.layer(options).pipe(Layer.provideMerge(storage));
835
836
  return Protocol.bind(protocol, group).pipe(Layer.provideMerge(storage));
836
837
  }
837
- //#endregion
838
838
  export { storageLayer as a, STORAGE_VERSION_TABLE as c, retryWithPolicy as d, transactionRetryPolicy as f, layerWithDurableObjectTransactions as i, clientLayer as l, layer as n, inspectFormat as o, layerProtocol as r, newerVersionFault as s, DEFAULT_REPAIR_INTERVAL_MS as t, sharedSqlClientLayer as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamsy/storage",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Effect SQL storage for Streamsy on SQLite, Postgres and MySQL, with Bun and Durable Object SQLite host layers.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,7 +42,7 @@
42
42
  "test:dialects": "bun --bun vitest run test/dialects"
43
43
  },
44
44
  "dependencies": {
45
- "@streamsy/core": "^0.4.0"
45
+ "@streamsy/core": "^0.6.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@cloudflare/workers-types": "4.20251228.0",
@@ -52,7 +52,7 @@
52
52
  "@effect/sql-sqlite-bun": "4.0.5",
53
53
  "@effect/sql-sqlite-do": "4.0.5",
54
54
  "@effect/vitest": "4.0.5",
55
- "@streamsy/serve": "0.4.0",
55
+ "@streamsy/serve": "0.6.0",
56
56
  "@types/bun": "^1.4.3",
57
57
  "effect": "4.0.5",
58
58
  "miniflare": "4.20260730.0",