@streamsy/storage 0.6.0 → 0.7.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/README.md +4 -0
- package/dist/bun.d.ts +3 -3
- package/dist/bun.js +2 -2
- package/dist/durable-object.d.ts +3 -3
- package/dist/durable-object.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{storage-Cb_kOO_g.d.ts → storage-C1XYywlK.d.ts} +3 -3
- package/dist/{storage-DFCZf1UK.js → storage-D5lTbsiK.js} +123 -15
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -122,6 +122,10 @@ In a Durable Object, use `DurableObjectStorage.layerProtocol` with
|
|
|
122
122
|
rejected without being changed. Use a fresh database path.
|
|
123
123
|
- Stream ids are limited to 512 characters, and producer ids to 255. A longer
|
|
124
124
|
id fails with a non-retryable `StorageFault` before anything is written.
|
|
125
|
+
- Projection checkpoint keys have no length limit. MySQL stores 512 key
|
|
126
|
+
characters, so on MySQL a longer key is stored as its first 447 characters,
|
|
127
|
+
`#` and the SHA-256 hex of the full key. The other dialects store every key
|
|
128
|
+
as it is.
|
|
125
129
|
- Bun defaults the client `busyTimeout` to zero and retries storage-owned
|
|
126
130
|
transactions with a bounded yielding policy. Tune `client.busyTimeout`,
|
|
127
131
|
`transactionRetryAttempts`, and `transactionRetryDelayMs` if you need to.
|
package/dist/bun.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as SqlStorageOptions } from "./storage-
|
|
1
|
+
import { r as SqlStorageOptions } from "./storage-C1XYywlK.js";
|
|
2
2
|
import { Layer } from "effect";
|
|
3
3
|
import { StorageFault, Streamsy } from "@streamsy/core";
|
|
4
4
|
import { Protocol } from "@streamsy/core/protocol";
|
|
@@ -12,8 +12,8 @@ interface BunStorageOptions extends SqlStorageOptions {
|
|
|
12
12
|
/** Options for `layer` and `layerProtocol`. */
|
|
13
13
|
interface Options extends BunStorageOptions, Protocol.ProtocolOptions {}
|
|
14
14
|
/** Official Bun SQLite host layer with one shared SqlClient/Reactivity graph. */
|
|
15
|
-
declare const storageLayer: (options: BunStorageOptions) => 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, StorageFault, never>;
|
|
15
|
+
declare const storageLayer: (options: BunStorageOptions) => Layer.Layer<import("@streamsy/core").CommitBoundary | import("@streamsy/core/internal").CommitHooks | import("@streamsy/core/internal").OwnerRecords | import("@streamsy/core/internal").ProjectionMarks | import("effect/reactivity/Reactivity").Reactivity | import("effect/sql/SqlClient").SqlClient | import("@streamsy/core").Storage, StorageFault, never>;
|
|
16
16
|
/** Complete persistent protocol Layer for an HttpRouter application. */
|
|
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>;
|
|
17
|
+
declare const layerProtocol: (options: Options) => Layer.Layer<import("@streamsy/core").CommitBoundary | import("@streamsy/core/internal").CommitHooks | import("@streamsy/core/internal").OwnerRecords | import("@streamsy/core/internal").ProjectionMarks | 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
19
|
export { bun_storage_d_exports as BunStorage };
|
package/dist/bun.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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-
|
|
2
|
+
import { a as storageLayer$1, d as retryWithPolicy, f as transactionRetryPolicy, o as inspectFormat, s as newerVersionFault, u as sharedSqlClientLayer } from "./storage-D5lTbsiK.js";
|
|
3
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";
|
|
@@ -26,7 +26,7 @@ const readFormat = (reader) => {
|
|
|
26
26
|
const preflight = (filename, driver, retry) => Effect.gen(function* () {
|
|
27
27
|
const state = yield* retryWithPolicy(driver.read(filename, readFormat), retry, (error) => error.retryable);
|
|
28
28
|
yield* inspectFormat(state.names);
|
|
29
|
-
if (state.version >
|
|
29
|
+
if (state.version > 2) return yield* newerVersionFault(state.version);
|
|
30
30
|
});
|
|
31
31
|
/**
|
|
32
32
|
* Prepare a SQLite file before the client opens it: create the parent folder,
|
package/dist/durable-object.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as SqlStorageOptions } from "./storage-
|
|
1
|
+
import { r as SqlStorageOptions } from "./storage-C1XYywlK.js";
|
|
2
2
|
import { Layer } from "effect";
|
|
3
3
|
import { Streamsy } from "@streamsy/core";
|
|
4
4
|
import { Protocol } from "@streamsy/core/protocol";
|
|
@@ -16,8 +16,8 @@ declare const DEFAULT_LONG_POLL_TIMEOUT_MS = 25000;
|
|
|
16
16
|
/** Options for `layer` and `layerProtocol`. */
|
|
17
17
|
interface Options extends DurableObjectStorageOptions, Protocol.ProtocolOptions {}
|
|
18
18
|
/** Official local/host Durable Object SQLite layer with commit-boundary support. */
|
|
19
|
-
declare const storageLayer: (options: DurableObjectStorageOptions) => 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").StorageFault, never>;
|
|
19
|
+
declare const storageLayer: (options: DurableObjectStorageOptions) => Layer.Layer<import("@streamsy/core").CommitBoundary | import("@streamsy/core/internal").CommitHooks | import("@streamsy/core/internal").OwnerRecords | import("@streamsy/core/internal").ProjectionMarks | import("effect/reactivity/Reactivity").Reactivity | import("effect/sql/SqlClient").SqlClient | import("@streamsy/core").Storage, import("@streamsy/core").StorageFault, never>;
|
|
20
20
|
/** Protocol over the official Durable Object SQLite layer. */
|
|
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>;
|
|
21
|
+
declare const layerProtocol: (options: Options) => Layer.Layer<import("@streamsy/core").CommitBoundary | import("@streamsy/core/internal").CommitHooks | import("@streamsy/core/internal").OwnerRecords | import("@streamsy/core/internal").ProjectionMarks | 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
23
|
export { durable_object_storage_d_exports as DurableObjectStorage };
|
package/dist/durable-object.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.js";
|
|
2
|
-
import { i as layerWithDurableObjectTransactions, u as sharedSqlClientLayer } from "./storage-
|
|
2
|
+
import { i as layerWithDurableObjectTransactions, u as sharedSqlClientLayer } from "./storage-D5lTbsiK.js";
|
|
3
3
|
import { Layer } from "effect";
|
|
4
4
|
import "@streamsy/core";
|
|
5
5
|
import { Protocol } from "@streamsy/core/protocol";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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-
|
|
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-C1XYywlK.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
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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-
|
|
2
|
+
import { l as clientLayer, n as layer, r as layerProtocol, t as DEFAULT_REPAIR_INTERVAL_MS } from "./storage-D5lTbsiK.js";
|
|
3
3
|
// src/sql-storage.ts
|
|
4
4
|
var sql_storage_exports = /* @__PURE__ */ __exportAll({
|
|
5
5
|
DEFAULT_REPAIR_INTERVAL_MS: () => DEFAULT_REPAIR_INTERVAL_MS,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Context, Effect, Layer, PubSub, Queue, Scope, Stream } from "effect";
|
|
2
2
|
import { Reactivity } from "effect/reactivity";
|
|
3
3
|
import { SqlClient, SqlError } from "effect/sql";
|
|
4
|
-
import { OwnerRecords } from "@streamsy/core/internal";
|
|
4
|
+
import { CommitHooks, OwnerRecords, ProjectionMarks } from "@streamsy/core/internal";
|
|
5
5
|
import { CommitBoundary, Storage, StorageFault, StreamId, Streamsy } from "@streamsy/core";
|
|
6
6
|
import { Protocol } from "@streamsy/core/protocol";
|
|
7
7
|
declare const DEFAULT_TRANSACTION_RETRY_ATTEMPTS = 16;
|
|
@@ -27,10 +27,10 @@ interface SqlStorageOptions {
|
|
|
27
27
|
readonly transactionRetryDelayMs?: number;
|
|
28
28
|
}
|
|
29
29
|
/** Generic SQLite-stream SQL layer. Hosts supply one matching SqlClient/Reactivity graph. */
|
|
30
|
-
declare const storageLayer: (options?: SqlStorageOptions) => Layer.Layer<Storage | CommitBoundary | OwnerRecords, StorageFault, SqlClient.SqlClient | Reactivity.Reactivity>;
|
|
30
|
+
declare const storageLayer: (options?: SqlStorageOptions) => Layer.Layer<Storage | CommitBoundary | OwnerRecords | CommitHooks | ProjectionMarks, StorageFault, SqlClient.SqlClient | Reactivity.Reactivity>;
|
|
31
31
|
/** Options for `layer` and `layerProtocol`. */
|
|
32
32
|
interface Options extends SqlStorageOptions, Protocol.ProtocolOptions {}
|
|
33
33
|
/** Protocol over SQL storage. */
|
|
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>;
|
|
34
|
+
declare const layerProtocol: (options?: Options) => Layer.Layer<CommitBoundary | CommitHooks | OwnerRecords | ProjectionMarks | Storage | import("@streamsy/core/protocol").StreamsReader | import("@streamsy/core/protocol").StreamsWriter, StorageFault, Reactivity.Reactivity | SqlClient.SqlClient>;
|
|
35
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>>>;
|
|
36
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,7 +1,7 @@
|
|
|
1
1
|
import { Array, Clock, Context, Deferred, Effect, Fiber, Layer, Option, Order, Predicate, PubSub, Queue, Schedule, Scheduler, Schema, Stream } from "effect";
|
|
2
2
|
import { Reactivity } from "effect/reactivity";
|
|
3
3
|
import { SqlClient, SqlError } from "effect/sql";
|
|
4
|
-
import { OwnerRecords, attachOwner, ownerOf } from "@streamsy/core/internal";
|
|
4
|
+
import { CommitHooks, MAX_PRODUCER_ID_LENGTH, OwnerRecords, ProjectionMarks, attachOwner, commitHookRegistry, fitKey, ownerOf } from "@streamsy/core/internal";
|
|
5
5
|
import { CommitBoundary, MutationRejected, Mutations, Offset, OperationResults, Storage, StorageFault, StreamId, ZERO_OFFSET } from "@streamsy/core";
|
|
6
6
|
import { Protocol } from "@streamsy/core/protocol";
|
|
7
7
|
const transactionRetryPolicy = (options) => {
|
|
@@ -43,6 +43,7 @@ const makeBoundary = (repairIntervalMs, transactionRetry, probe, transactionMaxO
|
|
|
43
43
|
const subscriptions = /* @__PURE__ */ new Set();
|
|
44
44
|
const feed = yield* Effect.acquireRelease(PubSub.unbounded(), PubSub.shutdown);
|
|
45
45
|
let closed = false;
|
|
46
|
+
const hooks = commitHookRegistry();
|
|
46
47
|
yield* Effect.addFinalizer(() => Effect.gen(function* () {
|
|
47
48
|
const active = yield* Effect.sync(() => {
|
|
48
49
|
closed = true;
|
|
@@ -58,7 +59,7 @@ const makeBoundary = (repairIntervalMs, transactionRetry, probe, transactionMaxO
|
|
|
58
59
|
ids: /* @__PURE__ */ new Set()
|
|
59
60
|
};
|
|
60
61
|
let bodyCompleted = false;
|
|
61
|
-
const transactionBody = body.pipe(Effect.provideService(PendingInvalidations, pending), Effect.tap(() => Effect.sync(() => {
|
|
62
|
+
const transactionBody = body.pipe(Effect.tap(() => hooks.run(pending.ids)), Effect.provideService(PendingInvalidations, pending), Effect.tap(() => Effect.sync(() => {
|
|
62
63
|
bodyCompleted = true;
|
|
63
64
|
})));
|
|
64
65
|
const transaction = sql.withTransaction(transactionBody).pipe(Effect.catchIf((error) => bodyCompleted && SqlError.isSqlError(error), (error) => Effect.die(error)));
|
|
@@ -140,7 +141,8 @@ const makeBoundary = (repairIntervalMs, transactionRetry, probe, transactionMaxO
|
|
|
140
141
|
withTransaction,
|
|
141
142
|
mutation,
|
|
142
143
|
changes,
|
|
143
|
-
committed: PubSub.subscribe(feed)
|
|
144
|
+
committed: PubSub.subscribe(feed),
|
|
145
|
+
beforeCommit: hooks.beforeCommit
|
|
144
146
|
};
|
|
145
147
|
});
|
|
146
148
|
const boundaryLayer = (repairIntervalMs, transactionRetry, probe, transactionMaxOpsBeforeYield) => Layer.effect(BoundaryRuntimeService, makeBoundary(repairIntervalMs, transactionRetry, probe, transactionMaxOpsBeforeYield));
|
|
@@ -164,7 +166,8 @@ const STORAGE_TABLES = [
|
|
|
164
166
|
"streamsy_streams",
|
|
165
167
|
"streamsy_messages",
|
|
166
168
|
"streamsy_producers",
|
|
167
|
-
"streamsy_owner_records"
|
|
169
|
+
"streamsy_owner_records",
|
|
170
|
+
"streamsy_projection_pending"
|
|
168
171
|
];
|
|
169
172
|
const STORAGE_VERSION_TABLE_DDL = {
|
|
170
173
|
sqlite: `CREATE TABLE ${STORAGE_VERSION_TABLE} (version INTEGER PRIMARY KEY, applied_at_ms INTEGER NOT NULL)`,
|
|
@@ -258,6 +261,23 @@ const STORAGE_MIGRATIONS = [{
|
|
|
258
261
|
owner_key VARCHAR(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin PRIMARY KEY, owner_value LONGTEXT NOT NULL
|
|
259
262
|
) ENGINE=InnoDB`
|
|
260
263
|
]
|
|
264
|
+
}, {
|
|
265
|
+
sqlite: [`CREATE TABLE streamsy_projection_pending (
|
|
266
|
+
projection TEXT NOT NULL, params TEXT NOT NULL, seq INTEGER NOT NULL,
|
|
267
|
+
attempts INTEGER NOT NULL DEFAULT 0, due_at INTEGER NOT NULL DEFAULT 0,
|
|
268
|
+
PRIMARY KEY(projection, params)
|
|
269
|
+
)`, `CREATE INDEX idx_streamsy_projection_pending_due ON streamsy_projection_pending(projection, due_at)`],
|
|
270
|
+
pg: [`CREATE TABLE streamsy_projection_pending (
|
|
271
|
+
projection TEXT COLLATE "C" NOT NULL, params TEXT COLLATE "C" NOT NULL, seq BIGINT NOT NULL,
|
|
272
|
+
attempts INTEGER NOT NULL DEFAULT 0, due_at BIGINT NOT NULL DEFAULT 0,
|
|
273
|
+
PRIMARY KEY(projection, params)
|
|
274
|
+
)`, `CREATE INDEX idx_streamsy_projection_pending_due ON streamsy_projection_pending(projection, due_at)`],
|
|
275
|
+
mysql: [`CREATE TABLE IF NOT EXISTS streamsy_projection_pending (
|
|
276
|
+
projection VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin NOT NULL, params VARCHAR(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin NOT NULL,
|
|
277
|
+
seq BIGINT NOT NULL, attempts INT NOT NULL DEFAULT 0, due_at BIGINT NOT NULL DEFAULT 0,
|
|
278
|
+
PRIMARY KEY(projection, params),
|
|
279
|
+
INDEX idx_streamsy_projection_pending_due (projection, due_at)
|
|
280
|
+
) ENGINE=InnoDB`]
|
|
261
281
|
}];
|
|
262
282
|
// src/migrations.ts
|
|
263
283
|
/** Fixed `pg_advisory_xact_lock` key: "streamsy" in ASCII, read as a 64-bit integer. */
|
|
@@ -265,13 +285,13 @@ const PG_MIGRATION_LOCK_KEY = "8319400191411975033";
|
|
|
265
285
|
/** Named lock that serialises MySQL migrations, whose DDL cannot share one transaction. */
|
|
266
286
|
const MYSQL_MIGRATION_LOCK = STORAGE_VERSION_TABLE;
|
|
267
287
|
const MYSQL_MIGRATION_LOCK_TIMEOUT_SECONDS = 30;
|
|
268
|
-
const fault = (operation, error) => new StorageFault({
|
|
288
|
+
const fault$1 = (operation, error) => new StorageFault({
|
|
269
289
|
operation,
|
|
270
290
|
message: error.message,
|
|
271
291
|
retryable: error.isRetryable,
|
|
272
292
|
cause: error
|
|
273
293
|
});
|
|
274
|
-
const sqlAttempt = (operation, effect) => effect.pipe(Effect.mapError((error) => fault(operation, error)));
|
|
294
|
+
const sqlAttempt = (operation, effect) => effect.pipe(Effect.mapError((error) => fault$1(operation, error)));
|
|
275
295
|
/** Return whether the database already holds this format, or fail on an older format. */
|
|
276
296
|
const inspectFormat = (tables) => {
|
|
277
297
|
const ownsFormat = tables.has(STORAGE_VERSION_TABLE);
|
|
@@ -283,7 +303,7 @@ const inspectFormat = (tables) => {
|
|
|
283
303
|
};
|
|
284
304
|
const newerVersionFault = (version) => new StorageFault({
|
|
285
305
|
operation: "migration.open",
|
|
286
|
-
message: `Unsupported newer Streamsy storage schema version ${version}; supported version is
|
|
306
|
+
message: `Unsupported newer Streamsy storage schema version ${version}; supported version is 2`,
|
|
287
307
|
retryable: false
|
|
288
308
|
});
|
|
289
309
|
const listTables = (sql, dialect) => sqlAttempt("migration.inspect", {
|
|
@@ -296,8 +316,8 @@ const listTables = (sql, dialect) => sqlAttempt("migration.inspect", {
|
|
|
296
316
|
const migrateVersions = (sql, dialect, hooks) => Effect.gen(function* () {
|
|
297
317
|
if (!(yield* inspectFormat(yield* listTables(sql, dialect)))) yield* sqlAttempt("migration.create-version-table", sql.unsafe(STORAGE_VERSION_TABLE_DDL[dialect]).pipe(Effect.asVoid));
|
|
298
318
|
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))));
|
|
299
|
-
if (current >
|
|
300
|
-
for (let version = current + 1; version <=
|
|
319
|
+
if (current > 2) return yield* newerVersionFault(current);
|
|
320
|
+
for (let version = current + 1; version <= 2; version++) {
|
|
301
321
|
if (hooks.beforeVersion !== void 0) yield* hooks.beforeVersion(version);
|
|
302
322
|
const statements = STORAGE_MIGRATIONS[version - 1]?.[dialect];
|
|
303
323
|
if (statements === void 0) return yield* Effect.die(/* @__PURE__ */ new Error("Missing migration"));
|
|
@@ -332,7 +352,86 @@ const migrate = (sql, hooks = {}, transactionRetry = transactionRetryPolicy({}))
|
|
|
332
352
|
hooks.onTransactionAttempt?.();
|
|
333
353
|
if (dialect === "mysql") return migrateMysql(sql, hooks);
|
|
334
354
|
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) => SqlError.isSqlError(error) ? error.isRetryable : error.retryable).pipe(Effect.catchIf(SqlError.isSqlError, (error) => Effect.fail(fault("migration.transaction", error)))));
|
|
355
|
+
}), transactionRetry, (error) => SqlError.isSqlError(error) ? error.isRetryable : error.retryable).pipe(Effect.catchIf(SqlError.isSqlError, (error) => Effect.fail(fault$1("migration.transaction", error)))));
|
|
356
|
+
// src/projection-marks.ts
|
|
357
|
+
const TABLE = "streamsy_projection_pending";
|
|
358
|
+
/**
|
|
359
|
+
* Durable Object SQL accepts at most 100 bound parameters in one statement. A
|
|
360
|
+
* marked row binds five values and a params lookup binds one, so both chunk.
|
|
361
|
+
*/
|
|
362
|
+
const MARK_CHUNK = 16;
|
|
363
|
+
const READ_CHUNK = 64;
|
|
364
|
+
const fault = (operation) => (error) => new StorageFault({
|
|
365
|
+
operation: `marks.${operation}`,
|
|
366
|
+
message: error.message,
|
|
367
|
+
retryable: error.isRetryable,
|
|
368
|
+
cause: error
|
|
369
|
+
});
|
|
370
|
+
const pendingMark = (row) => ({
|
|
371
|
+
params: row.params,
|
|
372
|
+
seq: Number(row.seq),
|
|
373
|
+
attempts: Number(row.attempts),
|
|
374
|
+
dueAt: Number(row.due_at)
|
|
375
|
+
});
|
|
376
|
+
/**
|
|
377
|
+
* Pending marks in `streamsy_projection_pending`. Statements join the boundary
|
|
378
|
+
* transaction when one is open, so a mark written by a commit hook commits or
|
|
379
|
+
* rolls back with the write that caused it.
|
|
380
|
+
*/
|
|
381
|
+
const sqlProjectionMarks = (sql) => {
|
|
382
|
+
const table = sql(TABLE);
|
|
383
|
+
const mark = (projection, params) => Effect.forEach(Array.chunksOf(params, MARK_CHUNK), (chunk) => {
|
|
384
|
+
const rows = sql.insert(Array.map(chunk, (value) => ({
|
|
385
|
+
projection,
|
|
386
|
+
params: value,
|
|
387
|
+
seq: 1,
|
|
388
|
+
attempts: 0,
|
|
389
|
+
due_at: 0
|
|
390
|
+
})));
|
|
391
|
+
return sql.onDialectOrElse({
|
|
392
|
+
mysql: () => sql`INSERT INTO ${table} ${rows} ON DUPLICATE KEY UPDATE seq = seq + 1`,
|
|
393
|
+
orElse: () => sql`INSERT INTO ${table} ${rows}
|
|
394
|
+
ON CONFLICT (projection, params) DO UPDATE SET seq = ${table}.seq + 1`
|
|
395
|
+
});
|
|
396
|
+
}, { discard: true });
|
|
397
|
+
const nextDue = (projection) => sql`SELECT MIN(due_at) AS at FROM ${table} WHERE projection = ${projection}`.pipe(Effect.map((rows) => {
|
|
398
|
+
const at = rows[0]?.at;
|
|
399
|
+
return at === null || at === void 0 ? Option.none() : Option.some(Number(at));
|
|
400
|
+
}), Effect.mapError(fault("nextDue")));
|
|
401
|
+
const due = (projection, now, limit) => sql`SELECT params, seq, attempts, due_at FROM ${table}
|
|
402
|
+
WHERE projection = ${projection} AND due_at <= ${now}
|
|
403
|
+
ORDER BY due_at, params LIMIT ${limit}`.pipe(Effect.map(Array.map(pendingMark)), Effect.mapError(fault("due")));
|
|
404
|
+
const read = (projection, params) => Effect.forEach(Array.chunksOf(params, READ_CHUNK), (chunk) => sql`SELECT params, seq, attempts, due_at FROM ${table}
|
|
405
|
+
WHERE projection = ${projection} AND ${sql.in("params", chunk)}`).pipe(Effect.map((chunks) => new Map(Array.map(chunks.flat(), (row) => [row.params, pendingMark(row)]))), Effect.mapError(fault("read")));
|
|
406
|
+
const clear = (projection, params, seq) => sql`DELETE FROM ${table} WHERE projection = ${projection} AND params = ${params} AND seq = ${seq}`.pipe(Effect.asVoid, Effect.mapError(fault("clear")));
|
|
407
|
+
const remove = (projection, params) => sql`DELETE FROM ${table} WHERE projection = ${projection} AND params = ${params}`.pipe(Effect.asVoid, Effect.mapError(fault("remove")));
|
|
408
|
+
const defer = (projection, params, attempts, dueAt) => {
|
|
409
|
+
const row = sql.insert({
|
|
410
|
+
projection,
|
|
411
|
+
params,
|
|
412
|
+
seq: 1,
|
|
413
|
+
attempts,
|
|
414
|
+
due_at: dueAt
|
|
415
|
+
});
|
|
416
|
+
return sql.onDialectOrElse({
|
|
417
|
+
mysql: () => sql`INSERT INTO ${table} ${row}
|
|
418
|
+
ON DUPLICATE KEY UPDATE attempts = VALUES(attempts), due_at = VALUES(due_at)`,
|
|
419
|
+
orElse: () => sql`INSERT INTO ${table} ${row}
|
|
420
|
+
ON CONFLICT (projection, params) DO UPDATE SET attempts = excluded.attempts, due_at = excluded.due_at`
|
|
421
|
+
}).pipe(Effect.asVoid, Effect.mapError(fault("defer")));
|
|
422
|
+
};
|
|
423
|
+
const deferred = (projection) => sql`SELECT COUNT(*) AS count FROM ${table} WHERE projection = ${projection} AND attempts > 0`.pipe(Effect.map((rows) => Number(rows[0]?.count ?? 0)), Effect.mapError(fault("deferred")));
|
|
424
|
+
return ProjectionMarks.of({
|
|
425
|
+
mark,
|
|
426
|
+
nextDue,
|
|
427
|
+
due,
|
|
428
|
+
read,
|
|
429
|
+
clear,
|
|
430
|
+
remove,
|
|
431
|
+
defer,
|
|
432
|
+
deferred
|
|
433
|
+
});
|
|
434
|
+
};
|
|
336
435
|
// src/storage.ts
|
|
337
436
|
const DEFAULT_REPAIR_INTERVAL_MS = 1e3;
|
|
338
437
|
const STORAGE_KEY = "streamsy:storage";
|
|
@@ -454,7 +553,7 @@ const tooLong = (operation, label, value, limit) => {
|
|
|
454
553
|
};
|
|
455
554
|
const oversizedKey = (mutation) => {
|
|
456
555
|
for (const operation of mutation.operations) {
|
|
457
|
-
const fault = Predicate.isTagged(operation, "Create") ? tooLong("mutate.stream-id", "Stream id", operation.record.id, 512) : Predicate.isTagged(operation, "Append") && operation.producer !== void 0 ? tooLong("mutate.producer-id", "Producer id", operation.producer.producerId,
|
|
556
|
+
const fault = Predicate.isTagged(operation, "Create") ? tooLong("mutate.stream-id", "Stream id", operation.record.id, 512) : Predicate.isTagged(operation, "Append") && operation.producer !== void 0 ? tooLong("mutate.producer-id", "Producer id", operation.producer.producerId, MAX_PRODUCER_ID_LENGTH) : Option.none();
|
|
458
557
|
if (Option.isSome(fault)) return fault;
|
|
459
558
|
}
|
|
460
559
|
return Option.none();
|
|
@@ -764,6 +863,10 @@ const OWNER_RECORD_CONFLICT = "records.conflict";
|
|
|
764
863
|
*/
|
|
765
864
|
const makeOwnerRecords = (sql) => {
|
|
766
865
|
const absentKeys = /* @__PURE__ */ new WeakMap();
|
|
866
|
+
const storedKey = (key) => sql.onDialectOrElse({
|
|
867
|
+
mysql: () => fitKey(key, 512),
|
|
868
|
+
orElse: () => Effect.succeed(key)
|
|
869
|
+
});
|
|
767
870
|
const select = (key, forUpdate) => attempt("records.read", sql`SELECT owner_value FROM streamsy_owner_records WHERE owner_key = ${key}${sql.literal(forUpdate ? " FOR UPDATE" : "")}`).pipe(Effect.map((rows) => Option.fromUndefinedOr(rows[0]?.owner_value)));
|
|
768
871
|
const lockFound = (key, found) => Option.isNone(found) ? Effect.succeed(found) : sql.onDialectOrElse({
|
|
769
872
|
sqlite: () => Effect.succeed(found),
|
|
@@ -791,7 +894,8 @@ const makeOwnerRecords = (sql) => {
|
|
|
791
894
|
cause: error
|
|
792
895
|
}) : sqlFault("records.write", error)), Effect.asVoid);
|
|
793
896
|
return OwnerRecords.of({
|
|
794
|
-
read: Effect.fn("SqlStorage.records.read")(function* (
|
|
897
|
+
read: Effect.fn("SqlStorage.records.read")(function* (requested) {
|
|
898
|
+
const key = yield* storedKey(requested);
|
|
795
899
|
const transaction = yield* Effect.serviceOption(sql.transactionService);
|
|
796
900
|
if (Option.isNone(transaction)) return yield* select(key, false);
|
|
797
901
|
const found = yield* Effect.flatMap(select(key, false), (first) => lockFound(key, first));
|
|
@@ -801,7 +905,8 @@ const makeOwnerRecords = (sql) => {
|
|
|
801
905
|
else absent.delete(key);
|
|
802
906
|
return found;
|
|
803
907
|
}),
|
|
804
|
-
write: Effect.fn("SqlStorage.records.write")(function* (
|
|
908
|
+
write: Effect.fn("SqlStorage.records.write")(function* (requested, encoded) {
|
|
909
|
+
const key = yield* storedKey(requested);
|
|
805
910
|
const transaction = yield* Effect.serviceOption(sql.transactionService);
|
|
806
911
|
const absent = Option.isSome(transaction) ? absentKeys.get(transaction.value) : void 0;
|
|
807
912
|
if (absent?.has(key) === true) {
|
|
@@ -809,7 +914,10 @@ const makeOwnerRecords = (sql) => {
|
|
|
809
914
|
absent.delete(key);
|
|
810
915
|
} else yield* upsert(key, encoded);
|
|
811
916
|
}),
|
|
812
|
-
remove: Effect.fn("SqlStorage.records.remove")((
|
|
917
|
+
remove: Effect.fn("SqlStorage.records.remove")(function* (requested) {
|
|
918
|
+
const key = yield* storedKey(requested);
|
|
919
|
+
yield* execute("records.remove", sql`DELETE FROM streamsy_owner_records WHERE owner_key = ${key}`);
|
|
920
|
+
})
|
|
813
921
|
});
|
|
814
922
|
};
|
|
815
923
|
const makeLayer = (options, probe, transactionMaxOpsBeforeYield) => {
|
|
@@ -821,7 +929,7 @@ const makeLayer = (options, probe, transactionMaxOpsBeforeYield) => {
|
|
|
821
929
|
} }, transactionRetry);
|
|
822
930
|
const boundary = yield* BoundaryRuntimeService;
|
|
823
931
|
const owner = ownerOf(boundary);
|
|
824
|
-
return Context.empty().pipe(Context.add(Storage, attachOwner(makeStorage(sql, boundary), owner)), Context.add(CommitBoundary, CommitBoundary.of(boundary)), Context.add(OwnerRecords, attachOwner(makeOwnerRecords(sql), owner)));
|
|
932
|
+
return Context.empty().pipe(Context.add(Storage, attachOwner(makeStorage(sql, boundary), owner)), Context.add(CommitBoundary, CommitBoundary.of(boundary)), Context.add(OwnerRecords, attachOwner(makeOwnerRecords(sql), owner)), Context.add(CommitHooks, attachOwner(CommitHooks.of({ beforeCommit: boundary.beforeCommit }), owner)), Context.add(ProjectionMarks, attachOwner(sqlProjectionMarks(sql), owner)));
|
|
825
933
|
})).pipe(Layer.provide(boundaryLayer(options.repairIntervalMs ?? 1e3, transactionRetry, probe, transactionMaxOpsBeforeYield)));
|
|
826
934
|
};
|
|
827
935
|
/** Generic SQLite-stream SQL layer. Hosts supply one matching SqlClient/Reactivity graph. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamsy/storage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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.
|
|
45
|
+
"@streamsy/core": "^0.7.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.
|
|
55
|
+
"@streamsy/serve": "0.7.0",
|
|
56
56
|
"@types/bun": "^1.4.3",
|
|
57
57
|
"effect": "4.0.5",
|
|
58
58
|
"miniflare": "4.20260730.0",
|