@streamsy/storage 0.5.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-kHdg037u.d.ts → storage-C1XYywlK.d.ts} +5 -5
- package/dist/{storage-D3Ubo2kB.js → storage-D5lTbsiK.js} +138 -22
- 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,8 +1,8 @@
|
|
|
1
|
-
import { Context, Effect, Layer, Queue, Scope, Stream } from "effect";
|
|
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";
|
|
5
|
-
import { CommitBoundary, Storage, StorageFault, Streamsy } from "@streamsy/core";
|
|
4
|
+
import { CommitHooks, OwnerRecords, ProjectionMarks } from "@streamsy/core/internal";
|
|
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;
|
|
8
8
|
declare const DEFAULT_TRANSACTION_RETRY_DELAY_MS = 25;
|
|
@@ -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
|
-
import { Array, Clock, Context, Deferred, Effect, Fiber, Layer, Option, Order, Predicate, Queue, Schedule, Scheduler, Schema, Stream } from "effect";
|
|
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) => {
|
|
@@ -41,7 +41,9 @@ const makeBoundary = (repairIntervalMs, transactionRetry, probe, transactionMaxO
|
|
|
41
41
|
const sql = yield* SqlClient.SqlClient;
|
|
42
42
|
const reactivity = yield* Reactivity.Reactivity;
|
|
43
43
|
const subscriptions = /* @__PURE__ */ new Set();
|
|
44
|
+
const feed = yield* Effect.acquireRelease(PubSub.unbounded(), PubSub.shutdown);
|
|
44
45
|
let closed = false;
|
|
46
|
+
const hooks = commitHookRegistry();
|
|
45
47
|
yield* Effect.addFinalizer(() => Effect.gen(function* () {
|
|
46
48
|
const active = yield* Effect.sync(() => {
|
|
47
49
|
closed = true;
|
|
@@ -52,17 +54,20 @@ const makeBoundary = (repairIntervalMs, transactionRetry, probe, transactionMaxO
|
|
|
52
54
|
}));
|
|
53
55
|
const ownedTransaction = (body) => Effect.suspend(() => {
|
|
54
56
|
if (probe !== void 0) probe.transactionAttempts += 1;
|
|
55
|
-
const pending =
|
|
57
|
+
const pending = {
|
|
58
|
+
keys: /* @__PURE__ */ new Set(),
|
|
59
|
+
ids: /* @__PURE__ */ new Set()
|
|
60
|
+
};
|
|
56
61
|
let bodyCompleted = false;
|
|
57
|
-
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(() => {
|
|
58
63
|
bodyCompleted = true;
|
|
59
64
|
})));
|
|
60
65
|
const transaction = sql.withTransaction(transactionBody).pipe(Effect.catchIf((error) => bodyCompleted && SqlError.isSqlError(error), (error) => Effect.die(error)));
|
|
61
66
|
return (transactionMaxOpsBeforeYield === void 0 ? transaction : transaction.pipe(Effect.provideService(Scheduler.MaxOpsBeforeYield, transactionMaxOpsBeforeYield))).pipe(Effect.tap(() => {
|
|
62
|
-
if (pending.size === 0) return Effect.void;
|
|
67
|
+
if (pending.keys.size === 0) return Effect.void;
|
|
63
68
|
if (probe !== void 0) probe.invalidations += 1;
|
|
64
|
-
return reactivity.invalidate([...pending]);
|
|
65
|
-
}));
|
|
69
|
+
return reactivity.invalidate([...pending.keys]);
|
|
70
|
+
}), Effect.tap(() => pending.ids.size === 0 ? Effect.void : PubSub.publish(feed, [...pending.ids])));
|
|
66
71
|
});
|
|
67
72
|
const withTransaction = (body) => Effect.flatMap(Effect.serviceOption(PendingInvalidations), (owned) => {
|
|
68
73
|
if (Option.isSome(owned)) return body;
|
|
@@ -71,12 +76,13 @@ const makeBoundary = (repairIntervalMs, transactionRetry, probe, transactionMaxO
|
|
|
71
76
|
return ownedTransaction(body);
|
|
72
77
|
});
|
|
73
78
|
});
|
|
74
|
-
const mutation = ({ keys, effect, retryable }) => Effect.flatMap(Effect.serviceOption(sql.transactionService), (ambient) => Effect.flatMap(Effect.serviceOption(PendingInvalidations), (pendingBefore) => {
|
|
79
|
+
const mutation = ({ keys, ids, effect, retryable }) => Effect.flatMap(Effect.serviceOption(sql.transactionService), (ambient) => Effect.flatMap(Effect.serviceOption(PendingInvalidations), (pendingBefore) => {
|
|
75
80
|
if (Option.isSome(ambient) && Option.isNone(pendingBefore)) return Effect.die(/* @__PURE__ */ new Error("Ambient SQL transaction is not owned by CommitBoundary"));
|
|
76
81
|
const run = Effect.tap(effect, () => Effect.flatMap(Effect.serviceOption(PendingInvalidations), (pending) => {
|
|
77
82
|
if (Option.isNone(pending)) return Effect.die(/* @__PURE__ */ new Error("CommitBoundary owner context disappeared"));
|
|
78
83
|
return Effect.sync(() => {
|
|
79
|
-
for (const key of keys) pending.value.add(key);
|
|
84
|
+
for (const key of keys) pending.value.keys.add(key);
|
|
85
|
+
for (const id of ids) pending.value.ids.add(id);
|
|
80
86
|
});
|
|
81
87
|
}));
|
|
82
88
|
return Option.isSome(ambient) ? run : retryWithPolicy(ownedTransaction(run), transactionRetry, (error) => SqlError.isSqlError(error) ? error.isRetryable : retryable(error));
|
|
@@ -134,7 +140,9 @@ const makeBoundary = (repairIntervalMs, transactionRetry, probe, transactionMaxO
|
|
|
134
140
|
return {
|
|
135
141
|
withTransaction,
|
|
136
142
|
mutation,
|
|
137
|
-
changes
|
|
143
|
+
changes,
|
|
144
|
+
committed: PubSub.subscribe(feed),
|
|
145
|
+
beforeCommit: hooks.beforeCommit
|
|
138
146
|
};
|
|
139
147
|
});
|
|
140
148
|
const boundaryLayer = (repairIntervalMs, transactionRetry, probe, transactionMaxOpsBeforeYield) => Layer.effect(BoundaryRuntimeService, makeBoundary(repairIntervalMs, transactionRetry, probe, transactionMaxOpsBeforeYield));
|
|
@@ -158,7 +166,8 @@ const STORAGE_TABLES = [
|
|
|
158
166
|
"streamsy_streams",
|
|
159
167
|
"streamsy_messages",
|
|
160
168
|
"streamsy_producers",
|
|
161
|
-
"streamsy_owner_records"
|
|
169
|
+
"streamsy_owner_records",
|
|
170
|
+
"streamsy_projection_pending"
|
|
162
171
|
];
|
|
163
172
|
const STORAGE_VERSION_TABLE_DDL = {
|
|
164
173
|
sqlite: `CREATE TABLE ${STORAGE_VERSION_TABLE} (version INTEGER PRIMARY KEY, applied_at_ms INTEGER NOT NULL)`,
|
|
@@ -252,6 +261,23 @@ const STORAGE_MIGRATIONS = [{
|
|
|
252
261
|
owner_key VARCHAR(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin PRIMARY KEY, owner_value LONGTEXT NOT NULL
|
|
253
262
|
) ENGINE=InnoDB`
|
|
254
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`]
|
|
255
281
|
}];
|
|
256
282
|
// src/migrations.ts
|
|
257
283
|
/** Fixed `pg_advisory_xact_lock` key: "streamsy" in ASCII, read as a 64-bit integer. */
|
|
@@ -259,13 +285,13 @@ const PG_MIGRATION_LOCK_KEY = "8319400191411975033";
|
|
|
259
285
|
/** Named lock that serialises MySQL migrations, whose DDL cannot share one transaction. */
|
|
260
286
|
const MYSQL_MIGRATION_LOCK = STORAGE_VERSION_TABLE;
|
|
261
287
|
const MYSQL_MIGRATION_LOCK_TIMEOUT_SECONDS = 30;
|
|
262
|
-
const fault = (operation, error) => new StorageFault({
|
|
288
|
+
const fault$1 = (operation, error) => new StorageFault({
|
|
263
289
|
operation,
|
|
264
290
|
message: error.message,
|
|
265
291
|
retryable: error.isRetryable,
|
|
266
292
|
cause: error
|
|
267
293
|
});
|
|
268
|
-
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)));
|
|
269
295
|
/** Return whether the database already holds this format, or fail on an older format. */
|
|
270
296
|
const inspectFormat = (tables) => {
|
|
271
297
|
const ownsFormat = tables.has(STORAGE_VERSION_TABLE);
|
|
@@ -277,7 +303,7 @@ const inspectFormat = (tables) => {
|
|
|
277
303
|
};
|
|
278
304
|
const newerVersionFault = (version) => new StorageFault({
|
|
279
305
|
operation: "migration.open",
|
|
280
|
-
message: `Unsupported newer Streamsy storage schema version ${version}; supported version is
|
|
306
|
+
message: `Unsupported newer Streamsy storage schema version ${version}; supported version is 2`,
|
|
281
307
|
retryable: false
|
|
282
308
|
});
|
|
283
309
|
const listTables = (sql, dialect) => sqlAttempt("migration.inspect", {
|
|
@@ -290,8 +316,8 @@ const listTables = (sql, dialect) => sqlAttempt("migration.inspect", {
|
|
|
290
316
|
const migrateVersions = (sql, dialect, hooks) => Effect.gen(function* () {
|
|
291
317
|
if (!(yield* inspectFormat(yield* listTables(sql, dialect)))) yield* sqlAttempt("migration.create-version-table", sql.unsafe(STORAGE_VERSION_TABLE_DDL[dialect]).pipe(Effect.asVoid));
|
|
292
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))));
|
|
293
|
-
if (current >
|
|
294
|
-
for (let version = current + 1; version <=
|
|
319
|
+
if (current > 2) return yield* newerVersionFault(current);
|
|
320
|
+
for (let version = current + 1; version <= 2; version++) {
|
|
295
321
|
if (hooks.beforeVersion !== void 0) yield* hooks.beforeVersion(version);
|
|
296
322
|
const statements = STORAGE_MIGRATIONS[version - 1]?.[dialect];
|
|
297
323
|
if (statements === void 0) return yield* Effect.die(/* @__PURE__ */ new Error("Missing migration"));
|
|
@@ -326,7 +352,86 @@ const migrate = (sql, hooks = {}, transactionRetry = transactionRetryPolicy({}))
|
|
|
326
352
|
hooks.onTransactionAttempt?.();
|
|
327
353
|
if (dialect === "mysql") return migrateMysql(sql, hooks);
|
|
328
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));
|
|
329
|
-
}), 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
|
+
};
|
|
330
435
|
// src/storage.ts
|
|
331
436
|
const DEFAULT_REPAIR_INTERVAL_MS = 1e3;
|
|
332
437
|
const STORAGE_KEY = "streamsy:storage";
|
|
@@ -448,7 +553,7 @@ const tooLong = (operation, label, value, limit) => {
|
|
|
448
553
|
};
|
|
449
554
|
const oversizedKey = (mutation) => {
|
|
450
555
|
for (const operation of mutation.operations) {
|
|
451
|
-
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();
|
|
452
557
|
if (Option.isSome(fault)) return fault;
|
|
453
558
|
}
|
|
454
559
|
return Option.none();
|
|
@@ -723,6 +828,7 @@ const makeStorage = (sql, boundary) => Storage.of({
|
|
|
723
828
|
if (Option.isSome(oversized)) return Effect.fail(oversized.value);
|
|
724
829
|
return boundary.mutation({
|
|
725
830
|
keys: [STORAGE_KEY],
|
|
831
|
+
ids,
|
|
726
832
|
effect: applyMutation(sql, mutation),
|
|
727
833
|
retryable: (error) => Predicate.isTagged(error, "StorageFault") && error.retryable
|
|
728
834
|
}).pipe(Effect.catchIf(SqlError.isSqlError, (error) => Effect.fail(sqlFault("mutate.transaction", error))));
|
|
@@ -731,6 +837,7 @@ const makeStorage = (sql, boundary) => Storage.of({
|
|
|
731
837
|
keys: [STORAGE_KEY],
|
|
732
838
|
read: snapshot(sql, id)
|
|
733
839
|
}),
|
|
840
|
+
committed: boundary.committed,
|
|
734
841
|
nextExpiry: attempt("nextExpiry", sql`SELECT expires_at_ms AS at, stream_id FROM streamsy_streams
|
|
735
842
|
WHERE expires_at_ms IS NOT NULL AND soft_deleted = 0
|
|
736
843
|
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({
|
|
@@ -756,6 +863,10 @@ const OWNER_RECORD_CONFLICT = "records.conflict";
|
|
|
756
863
|
*/
|
|
757
864
|
const makeOwnerRecords = (sql) => {
|
|
758
865
|
const absentKeys = /* @__PURE__ */ new WeakMap();
|
|
866
|
+
const storedKey = (key) => sql.onDialectOrElse({
|
|
867
|
+
mysql: () => fitKey(key, 512),
|
|
868
|
+
orElse: () => Effect.succeed(key)
|
|
869
|
+
});
|
|
759
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)));
|
|
760
871
|
const lockFound = (key, found) => Option.isNone(found) ? Effect.succeed(found) : sql.onDialectOrElse({
|
|
761
872
|
sqlite: () => Effect.succeed(found),
|
|
@@ -783,7 +894,8 @@ const makeOwnerRecords = (sql) => {
|
|
|
783
894
|
cause: error
|
|
784
895
|
}) : sqlFault("records.write", error)), Effect.asVoid);
|
|
785
896
|
return OwnerRecords.of({
|
|
786
|
-
read: Effect.fn("SqlStorage.records.read")(function* (
|
|
897
|
+
read: Effect.fn("SqlStorage.records.read")(function* (requested) {
|
|
898
|
+
const key = yield* storedKey(requested);
|
|
787
899
|
const transaction = yield* Effect.serviceOption(sql.transactionService);
|
|
788
900
|
if (Option.isNone(transaction)) return yield* select(key, false);
|
|
789
901
|
const found = yield* Effect.flatMap(select(key, false), (first) => lockFound(key, first));
|
|
@@ -793,7 +905,8 @@ const makeOwnerRecords = (sql) => {
|
|
|
793
905
|
else absent.delete(key);
|
|
794
906
|
return found;
|
|
795
907
|
}),
|
|
796
|
-
write: Effect.fn("SqlStorage.records.write")(function* (
|
|
908
|
+
write: Effect.fn("SqlStorage.records.write")(function* (requested, encoded) {
|
|
909
|
+
const key = yield* storedKey(requested);
|
|
797
910
|
const transaction = yield* Effect.serviceOption(sql.transactionService);
|
|
798
911
|
const absent = Option.isSome(transaction) ? absentKeys.get(transaction.value) : void 0;
|
|
799
912
|
if (absent?.has(key) === true) {
|
|
@@ -801,7 +914,10 @@ const makeOwnerRecords = (sql) => {
|
|
|
801
914
|
absent.delete(key);
|
|
802
915
|
} else yield* upsert(key, encoded);
|
|
803
916
|
}),
|
|
804
|
-
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
|
+
})
|
|
805
921
|
});
|
|
806
922
|
};
|
|
807
923
|
const makeLayer = (options, probe, transactionMaxOpsBeforeYield) => {
|
|
@@ -813,7 +929,7 @@ const makeLayer = (options, probe, transactionMaxOpsBeforeYield) => {
|
|
|
813
929
|
} }, transactionRetry);
|
|
814
930
|
const boundary = yield* BoundaryRuntimeService;
|
|
815
931
|
const owner = ownerOf(boundary);
|
|
816
|
-
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)));
|
|
817
933
|
})).pipe(Layer.provide(boundaryLayer(options.repairIntervalMs ?? 1e3, transactionRetry, probe, transactionMaxOpsBeforeYield)));
|
|
818
934
|
};
|
|
819
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",
|