@rikalabs/akter 0.1.0-alpha.4 → 0.1.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/actor/descriptor.js +3 -0
- package/dist/actor/handles.d.ts +1 -1
- package/dist/actor/layers.js +2 -0
- package/dist/actor/reads.js +2 -1
- package/dist/contexts/command.d.ts +2 -0
- package/dist/identity/caller.d.ts +4 -4
- package/dist/identity/caller.js +11 -2
- package/dist/identity/mint.d.ts +1 -1
- package/dist/policies/command.d.ts +3 -0
- package/dist/policies/command.js +1 -0
- package/dist/runtime/connections/owner.js +60 -13
- package/dist/runtime/connections/protocol.d.ts +2 -2
- package/dist/runtime/connections/streams.js +2 -1
- package/dist/runtime/database/bounded.js +17 -2
- package/dist/runtime/database/connection.d.ts +3 -2
- package/dist/runtime/database/connection.js +9 -4
- package/dist/runtime/database/coordination.d.ts +6 -3
- package/dist/runtime/database/coordination.js +19 -18
- package/dist/runtime/database/migrations.d.ts +4 -0
- package/dist/runtime/database/migrations.js +72 -1
- package/dist/runtime/entity/mailbox.d.ts +18 -10
- package/dist/runtime/entity/mailbox.js +42 -34
- package/dist/runtime/entity/register.d.ts +3 -3
- package/dist/runtime/entity/register.js +280 -271
- package/dist/runtime/events/append.d.ts +3 -3
- package/dist/runtime/index.d.ts +4 -1
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/layer.d.ts +24 -1
- package/dist/runtime/layer.js +64 -9
- package/dist/runtime/members.d.ts +2 -0
- package/dist/runtime/operators/export.d.ts +3 -1
- package/dist/runtime/operators/export.js +42 -11
- package/dist/runtime/queries.d.ts +1 -1
- package/dist/runtime/queries.js +23 -6
- package/dist/runtime/registration.js +27 -1
- package/dist/runtime/request.d.ts +1 -1
- package/dist/runtime/runner.d.ts +18 -1
- package/dist/runtime/runner.js +24 -4
- package/dist/runtime/storage/cold-storage.d.ts +56 -0
- package/dist/runtime/storage/cold-storage.js +138 -0
- package/dist/runtime/storage/cold-tier.d.ts +141 -0
- package/dist/runtime/storage/cold-tier.js +355 -0
- package/dist/runtime/storage/generation.d.ts +14 -0
- package/dist/runtime/storage/generation.js +2 -0
- package/dist/runtime/storage/placements.js +1 -0
- package/dist/runtime/telemetry/metrics.d.ts +4 -0
- package/dist/runtime/telemetry/metrics.js +20 -4
- package/dist/runtime/telemetry/sampler.js +4 -3
- package/dist/runtime/telemetry/spans.d.ts +1 -1
- package/dist/runtime/topology/locks.js +8 -6
- package/dist/runtime/turn/blobs.d.ts +4 -1
- package/dist/runtime/turn/blobs.js +19 -2
- package/dist/runtime/turn/execute.d.ts +3 -1
- package/dist/runtime/turn/execute.js +272 -20
- package/dist/runtime/turn/hooks.d.ts +1 -1
- package/dist/runtime/turn/outbox.d.ts +1 -1
- package/dist/runtime/turn/receipt.d.ts +1 -1
- package/dist/runtime/turn/relay.d.ts +2 -2
- package/dist/runtime/turn/relay.js +53 -7
- package/dist/testing/actor-test.d.ts +1 -0
- package/dist/testing/actor-test.js +4 -1
- package/package.json +1 -1
package/dist/runtime/layer.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Actors } from "../handles/actors.ts";
|
|
|
5
5
|
import { InternalActors } from "./actors.ts";
|
|
6
6
|
import type { AccessRequest } from "../policies/access.ts";
|
|
7
7
|
import { pglite } from "./database/pglite.ts";
|
|
8
|
+
import { type ColdStorageOptions } from "./storage/cold-tier.ts";
|
|
8
9
|
import { DefectLog } from "./telemetry/defects.ts";
|
|
9
10
|
import { OperatorRuntime } from "./operators/repair.ts";
|
|
10
11
|
import { TelemetrySampler } from "./telemetry/sampler.ts";
|
|
@@ -15,6 +16,8 @@ import { RuntimeControl } from "./drain.ts";
|
|
|
15
16
|
import type { AnyFleetView } from "../tables/fleet.ts";
|
|
16
17
|
/** Configuration for `Actors.layer`: authorization, actor and effect layers, timing, retention, and row-level security. */
|
|
17
18
|
export interface Options {
|
|
19
|
+
/** Opt-in cold state and actor blobs. Postgres only; backup retention is required. */
|
|
20
|
+
readonly coldStorage?: ColdStorageOptions;
|
|
18
21
|
/**
|
|
19
22
|
* The global authorization hook, asked about every external request beside
|
|
20
23
|
* the actor's own `access` policy; when both exist both must allow. With
|
|
@@ -223,6 +226,10 @@ export interface Options {
|
|
|
223
226
|
*/
|
|
224
227
|
export declare const layer: (options?: Options) => Layer.Layer<Actors | ContentStore | DefectLog | InternalActors | OperatorRuntime | RuntimeControl | TelemetrySampler, import("effect/sql/Migrator").MigrationError | SqlError.SqlError, Crypto.Crypto | SqlClient.SqlClient>;
|
|
225
228
|
type PostgresOptions = Omit<PgClient.PgPoolConfig, "types"> & {
|
|
229
|
+
/** Opt-in transaction pooling for socket runners with table leases; session-dependent features are refused. */
|
|
230
|
+
readonly pooling?: "transaction";
|
|
231
|
+
/** Four primary sessions: one turn, two off-turn (one reserved by Cluster), and one query. Explicit pool sizes take precedence. */
|
|
232
|
+
readonly preset?: "low-connection";
|
|
226
233
|
readonly offTurnConnections?: number;
|
|
227
234
|
readonly queryConnections?: number;
|
|
228
235
|
readonly replica?: Omit<PgClient.PgPoolConfig, "types"> | undefined;
|
|
@@ -245,11 +252,27 @@ type PostgresOptions = Omit<PgClient.PgPoolConfig, "types"> & {
|
|
|
245
252
|
* first come, first served. Keep the sum across runners below the server's
|
|
246
253
|
* `max_connections`.
|
|
247
254
|
*
|
|
255
|
+
* `preset: "low-connection"` defaults these pools to 1/2/1 instead of
|
|
256
|
+
* 50/10/10. Cluster reserves one off-turn session for its entire lifetime,
|
|
257
|
+
* including with table leases, so the other must remain available for
|
|
258
|
+
* startup and ordinary work. Fleet maintenance and application reservations
|
|
259
|
+
* need additional off-turn capacity; optional pools add to the budget.
|
|
260
|
+
* Explicit pool sizes override the preset.
|
|
261
|
+
*
|
|
262
|
+
* `pooling: "transaction"` explicitly selects a transaction-pooled endpoint.
|
|
263
|
+
* It requires `Runner.socket` table leases, refuses replica, separate
|
|
264
|
+
* coordination, fleet and cold storage, disables prepared statements and
|
|
265
|
+
* warm turn speculation, and does not inject server keepalive startup
|
|
266
|
+
* parameters. Configure both proxy transport legs separately. Cluster's
|
|
267
|
+
* reservation consumes a logical client, not a backend outside transactions.
|
|
268
|
+
* Application session state, `listen` and session advisory locks remain
|
|
269
|
+
* unsupported on this endpoint.
|
|
270
|
+
*
|
|
248
271
|
* `replica` is this runner's nearest streaming replica of the same primary.
|
|
249
272
|
* Queries read there once it has replayed the commit version their caller
|
|
250
273
|
* last saw, and read the primary when it is behind or fails. Its pool
|
|
251
274
|
* (`maxConnections` default 10) opens connections only as queries need them.
|
|
252
|
-
*
|
|
275
|
+
* Direct pools request server TCP keepalives at 5 seconds idle, 2 seconds
|
|
253
276
|
* between probes, and 3 probes. `startupParameters` overrides these defaults
|
|
254
277
|
* independently on the primary and replica configurations.
|
|
255
278
|
*
|
package/dist/runtime/layer.js
CHANGED
|
@@ -15,6 +15,7 @@ import { schemaChange } from "./database/schema.js";
|
|
|
15
15
|
import { queryPoolLayer, ReadReplica, replicaLayer } from "./database/replica.js";
|
|
16
16
|
import { Coordination, coordinationLayer } from "./database/coordination.js";
|
|
17
17
|
import { withKeepalives } from "./database/keepalive.js";
|
|
18
|
+
import { WarmTurnFastPath } from "./turn/execute.js";
|
|
18
19
|
import { checkRowLevelSecurity, TenantScope } from "./database/tenancy.js";
|
|
19
20
|
import { retryDelay } from "./retry.js";
|
|
20
21
|
import { withoutDatabase } from "./jobs/isolation.js";
|
|
@@ -26,6 +27,7 @@ import { ProgressSink, ProgressTap } from "./jobs/progress.js";
|
|
|
26
27
|
import { FEED_MEMBER, isWatchMember, watchedQuery } from "./connections/protocol.js";
|
|
27
28
|
import { checkIdentity, databaseTime, FrameworkClock } from "./turn/admission.js";
|
|
28
29
|
import { routingKey } from "./storage/codec.js";
|
|
30
|
+
import { ColdTier, coldTier } from "./storage/cold-tier.js";
|
|
29
31
|
import { recordedPlacement } from "./storage/placements.js";
|
|
30
32
|
import { TurnHooks } from "./turn/hooks.js";
|
|
31
33
|
import { requestAttributes, SpanNames } from "./telemetry/spans.js";
|
|
@@ -220,7 +222,17 @@ export const layer = (options = {}) => {
|
|
|
220
222
|
const frameworkClock = yield* FrameworkClock;
|
|
221
223
|
const writerDeclarations = [];
|
|
222
224
|
let refreshedAt;
|
|
223
|
-
|
|
225
|
+
let services = yield* Effect.context();
|
|
226
|
+
if (options.coldStorage !== undefined &&
|
|
227
|
+
Option.isSome(yield* Effect.serviceOption(PgliteClient.PgliteClient)))
|
|
228
|
+
return yield* Effect.die(new Error("coldStorage requires Postgres, not PGlite"));
|
|
229
|
+
const tier = options.coldStorage === undefined
|
|
230
|
+
? undefined
|
|
231
|
+
: yield* coldTier(options.coldStorage, (yield* (yield* SqlClient.SqlClient) `SELECT deployment_id FROM actor_deployment`)[0].deployment_id, relaySettings.maxBackoffMs);
|
|
232
|
+
services = Context.add(services, ColdTier, tier);
|
|
233
|
+
const coldSweep = tier?.periodic === true
|
|
234
|
+
? yield* tier.run.pipe(Effect.provideContext(services), Effect.forkIn(scope))
|
|
235
|
+
: undefined;
|
|
224
236
|
const defectLog = boundedDefectLog(defectCapacity);
|
|
225
237
|
let recorder;
|
|
226
238
|
const currentRecorder = () => recorder;
|
|
@@ -582,7 +594,7 @@ export const layer = (options = {}) => {
|
|
|
582
594
|
}, () => new Map(Array.from(registrations.values(), ({ name, cron, policy }) => [
|
|
583
595
|
name,
|
|
584
596
|
{ entries: cron, skipMs: policy.cronSkipMs },
|
|
585
|
-
]))).pipe(Effect.provideService(ProgressSink, progressSink));
|
|
597
|
+
]))).pipe(Effect.provideService(ProgressSink, progressSink), Effect.provideService(ColdTier, tier));
|
|
586
598
|
yield* relay.run.pipe(Effect.forkIn(scope));
|
|
587
599
|
let fleetSubscribe;
|
|
588
600
|
if (options.fleet !== undefined && options.fleet.length > 0) {
|
|
@@ -785,7 +797,7 @@ export const layer = (options = {}) => {
|
|
|
785
797
|
stopClaims: relay.stop,
|
|
786
798
|
attemptsIdle: relay.attemptsIdle,
|
|
787
799
|
interruptAttempts: relay.interruptAttempts,
|
|
788
|
-
stopBackground: sweeping
|
|
800
|
+
stopBackground: Effect.forEach([sweeping, coldSweep].filter((fiber) => fiber !== undefined), Fiber.interrupt, { discard: true }),
|
|
789
801
|
serving,
|
|
790
802
|
scope,
|
|
791
803
|
});
|
|
@@ -839,12 +851,19 @@ export const layer = (options = {}) => {
|
|
|
839
851
|
}));
|
|
840
852
|
}),
|
|
841
853
|
});
|
|
842
|
-
return Context.make(Actors, publicActors).pipe(Context.add(ContentStore, contentStore), Context.add(InternalActors, internalActors), Context.add(RuntimeControl, control), Context.add(DefectLog, defectLog), Context.add(TelemetrySampler, TelemetrySampler.of({ sample })), Context.add(OperatorRuntime, operators));
|
|
854
|
+
return Context.make(Actors, publicActors).pipe(Context.add(ColdTier, tier), Context.add(ContentStore, contentStore), Context.add(InternalActors, internalActors), Context.add(RuntimeControl, control), Context.add(DefectLog, defectLog), Context.add(TelemetrySampler, TelemetrySampler.of({ sample })), Context.add(OperatorRuntime, operators));
|
|
843
855
|
}));
|
|
844
856
|
return Layer.unwrap(Effect.gen(function* () {
|
|
845
857
|
const sql = yield* SqlClient.SqlClient;
|
|
846
858
|
const coordination = (yield* Coordination) ?? sql;
|
|
847
859
|
const wiring = Option.getOrUndefined(yield* Effect.serviceOption(RunnerWiring));
|
|
860
|
+
const transactionPooling = yield* TransactionPooling;
|
|
861
|
+
if (transactionPooling &&
|
|
862
|
+
(wiring?.production !== true ||
|
|
863
|
+
wiring.config?.shardLockDisableAdvisory !== true ||
|
|
864
|
+
options.coldStorage !== undefined ||
|
|
865
|
+
(options.fleet?.length ?? 0) > 0))
|
|
866
|
+
return yield* Effect.die(new Error("Transaction pooling requires Runner.socket table leases and does not support coldStorage or fleet"));
|
|
848
867
|
yield* migrate;
|
|
849
868
|
if (options.rowLevelSecurity !== undefined &&
|
|
850
869
|
options.adoption !== undefined &&
|
|
@@ -889,7 +908,7 @@ export const layer = (options = {}) => {
|
|
|
889
908
|
expiration: Duration.fromInputUnsafe(config.shardLockExpiration ?? ShardingConfig.defaults.shardLockExpiration),
|
|
890
909
|
}))
|
|
891
910
|
: Layer.empty;
|
|
892
|
-
return runtime.pipe(Layer.provide(sharding), Layer.provide(lease), Layer.provide(Layer.succeed(TenantScope, {
|
|
911
|
+
return runtime.pipe(Layer.provide(transactionPooling ? Layer.succeed(WarmTurnFastPath, false) : Layer.empty), Layer.provide(sharding), Layer.provide(lease), Layer.provide(Layer.succeed(TenantScope, {
|
|
893
912
|
role: options.rowLevelSecurity?.role,
|
|
894
913
|
adoption: options.adoption === undefined
|
|
895
914
|
? undefined
|
|
@@ -897,6 +916,10 @@ export const layer = (options = {}) => {
|
|
|
897
916
|
})));
|
|
898
917
|
}));
|
|
899
918
|
};
|
|
919
|
+
/** Explicit pooling mode, independent of endpoint names or URL parameters. */
|
|
920
|
+
const TransactionPooling = Context.Reference("akter/TransactionPooling", {
|
|
921
|
+
defaultValue: () => false,
|
|
922
|
+
});
|
|
900
923
|
/**
|
|
901
924
|
* A runner holds three primary pools, plus optional replica and coordination
|
|
902
925
|
* pools. Turns lease sessions from the turn pool, `maxConnections` (default
|
|
@@ -913,11 +936,27 @@ export const layer = (options = {}) => {
|
|
|
913
936
|
* first come, first served. Keep the sum across runners below the server's
|
|
914
937
|
* `max_connections`.
|
|
915
938
|
*
|
|
939
|
+
* `preset: "low-connection"` defaults these pools to 1/2/1 instead of
|
|
940
|
+
* 50/10/10. Cluster reserves one off-turn session for its entire lifetime,
|
|
941
|
+
* including with table leases, so the other must remain available for
|
|
942
|
+
* startup and ordinary work. Fleet maintenance and application reservations
|
|
943
|
+
* need additional off-turn capacity; optional pools add to the budget.
|
|
944
|
+
* Explicit pool sizes override the preset.
|
|
945
|
+
*
|
|
946
|
+
* `pooling: "transaction"` explicitly selects a transaction-pooled endpoint.
|
|
947
|
+
* It requires `Runner.socket` table leases, refuses replica, separate
|
|
948
|
+
* coordination, fleet and cold storage, disables prepared statements and
|
|
949
|
+
* warm turn speculation, and does not inject server keepalive startup
|
|
950
|
+
* parameters. Configure both proxy transport legs separately. Cluster's
|
|
951
|
+
* reservation consumes a logical client, not a backend outside transactions.
|
|
952
|
+
* Application session state, `listen` and session advisory locks remain
|
|
953
|
+
* unsupported on this endpoint.
|
|
954
|
+
*
|
|
916
955
|
* `replica` is this runner's nearest streaming replica of the same primary.
|
|
917
956
|
* Queries read there once it has replayed the commit version their caller
|
|
918
957
|
* last saw, and read the primary when it is behind or fails. Its pool
|
|
919
958
|
* (`maxConnections` default 10) opens connections only as queries need them.
|
|
920
|
-
*
|
|
959
|
+
* Direct pools request server TCP keepalives at 5 seconds idle, 2 seconds
|
|
921
960
|
* between probes, and 3 probes. `startupParameters` overrides these defaults
|
|
922
961
|
* independently on the primary and replica configurations.
|
|
923
962
|
*
|
|
@@ -925,6 +964,9 @@ export const layer = (options = {}) => {
|
|
|
925
964
|
* migrator needs it on restart; remove once Effect #8309 lands.
|
|
926
965
|
*/
|
|
927
966
|
function postgres(options) {
|
|
967
|
+
if (options.pooling === "transaction" &&
|
|
968
|
+
(options.replica !== undefined || options.coordination !== undefined))
|
|
969
|
+
throw new Error("Transaction pooling does not support replica or separate coordination");
|
|
928
970
|
const types = PgTypes.makeRegistry();
|
|
929
971
|
types.register(2205, {
|
|
930
972
|
encode: (value) => PgTypes.encode(value, PgTypes.OID.oid),
|
|
@@ -932,9 +974,22 @@ function postgres(options) {
|
|
|
932
974
|
? Result.succeed(new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0))
|
|
933
975
|
: Result.fail(new PgTypes.CodecError({ message: "Invalid regclass value" })),
|
|
934
976
|
});
|
|
935
|
-
const { offTurnConnections, queryConnections, replica, coordination, ...configured } = options;
|
|
936
|
-
const pool = withKeepalives(configured);
|
|
937
|
-
|
|
977
|
+
const { pooling, preset, offTurnConnections, queryConnections, replica, coordination, ...configured } = options;
|
|
978
|
+
const pool = pooling === "transaction" ? { ...configured, prepare: false } : withKeepalives(configured);
|
|
979
|
+
const lowConnection = preset === "low-connection";
|
|
980
|
+
return Layer.mergeAll(Layer.succeed(TransactionPooling, pooling === "transaction"), boundedLayer({
|
|
981
|
+
...pool,
|
|
982
|
+
maxConnections: offTurnConnections ?? (lowConnection ? 2 : 10),
|
|
983
|
+
types,
|
|
984
|
+
}), turnGroups.pipe(Layer.provideMerge(turnConnections({
|
|
985
|
+
...pool,
|
|
986
|
+
maxConnections: pool.maxConnections ?? (lowConnection ? 1 : 50),
|
|
987
|
+
types,
|
|
988
|
+
}))), queryPoolLayer({
|
|
989
|
+
...pool,
|
|
990
|
+
maxConnections: queryConnections ?? (lowConnection ? 1 : 10),
|
|
991
|
+
types,
|
|
992
|
+
}), replicaLayer(replica === undefined ? undefined : { ...withKeepalives(replica), types }), coordinationLayer(coordination === undefined ? undefined : { ...withKeepalives(coordination), types }));
|
|
938
993
|
}
|
|
939
994
|
/** Database layers for `Actors.layer`: `postgres` for real deployments, `pglite` for embedded and test use. */
|
|
940
995
|
export const Database = {
|
|
@@ -279,6 +279,7 @@ export interface RegisteredQuery {
|
|
|
279
279
|
/** What an actor type's query layer registers with the runtime. */
|
|
280
280
|
export interface QueryRegistration {
|
|
281
281
|
readonly name: string;
|
|
282
|
+
readonly stateVersion: number;
|
|
282
283
|
readonly placement: Placement;
|
|
283
284
|
/** `commandTimeout`: a query's reads are cancelled on the server past it. */
|
|
284
285
|
readonly timeoutMs: number;
|
|
@@ -293,6 +294,7 @@ export interface QueryRegistration {
|
|
|
293
294
|
/** What an actor type's command layer registers with the runtime. */
|
|
294
295
|
export interface Registration {
|
|
295
296
|
readonly name: string;
|
|
297
|
+
readonly stateVersion: number;
|
|
296
298
|
readonly singleton: boolean;
|
|
297
299
|
/** Unkeyed with `policy.createdBy`: its UUIDv8 ids come only from `turn.mint`. */
|
|
298
300
|
readonly mintable: boolean;
|
|
@@ -21,7 +21,9 @@ export declare class ExportRefused extends ExportRefused_base {
|
|
|
21
21
|
* them in `omitted`. Subscription cursors and connections are neither carried
|
|
22
22
|
* nor counted. A job whose attempt has been cancelled or has finally failed
|
|
23
23
|
* is not pending and is left out. It reads the runtime tables, not the
|
|
24
|
-
* inspection views, because the views do not show a cancelled job.
|
|
24
|
+
* inspection views, because the views do not show a cancelled job. A cold
|
|
25
|
+
* pointer is captured in that snapshot; its immutable material is fetched
|
|
26
|
+
* only after the transaction ends, without activation or write-back. `None`
|
|
25
27
|
* when the tenant has no such actor.
|
|
26
28
|
*/
|
|
27
29
|
export declare const exportActor: (page: {
|
|
@@ -5,6 +5,7 @@ import { JOB_KEY_PREFIX } from "../../handles/intents.js";
|
|
|
5
5
|
import { decodeBytes, decodeText } from "../inspector/queries.js";
|
|
6
6
|
import { inReadOnlySnapshot } from "../database/snapshot.js";
|
|
7
7
|
import { TenantScope, tenantSettings } from "../database/tenancy.js";
|
|
8
|
+
import { ColdTier } from "../storage/cold-tier.js";
|
|
8
9
|
import { databaseTime } from "../turn/admission.js";
|
|
9
10
|
import { SEED_FORMAT } from "./seed.js";
|
|
10
11
|
/** The most pending intents, and the most pending jobs, one export carries. */
|
|
@@ -41,18 +42,21 @@ const tenantSnapshot = (tenant) => (effect) => Effect.gen(function* () {
|
|
|
41
42
|
* them in `omitted`. Subscription cursors and connections are neither carried
|
|
42
43
|
* nor counted. A job whose attempt has been cancelled or has finally failed
|
|
43
44
|
* is not pending and is left out. It reads the runtime tables, not the
|
|
44
|
-
* inspection views, because the views do not show a cancelled job.
|
|
45
|
+
* inspection views, because the views do not show a cancelled job. A cold
|
|
46
|
+
* pointer is captured in that snapshot; its immutable material is fetched
|
|
47
|
+
* only after the transaction ends, without activation or write-back. `None`
|
|
45
48
|
* when the tenant has no such actor.
|
|
46
49
|
*/
|
|
47
50
|
export const exportActor = (page) => tenantSnapshot(page.tenant)(Effect.gen(function* () {
|
|
48
51
|
const sql = yield* SqlClient.SqlClient;
|
|
49
52
|
const [found] = yield* sql `
|
|
50
|
-
SELECT routing_key::text AS "routingKey", created
|
|
53
|
+
SELECT routing_key::text AS "routingKey", created, cold_ref, cold_digest, cold_state_version
|
|
54
|
+
FROM actor_generations
|
|
51
55
|
WHERE tenant_id = ${page.tenant} AND actor_type = ${page.actorType}
|
|
52
56
|
AND actor_id = ${page.actorId}`;
|
|
53
57
|
if (found === undefined)
|
|
54
|
-
return
|
|
55
|
-
const { routingKey
|
|
58
|
+
return undefined;
|
|
59
|
+
const { routingKey } = found;
|
|
56
60
|
const owned = sql `routing_key = ${routingKey}::int8 AND tenant_id = ${page.tenant}
|
|
57
61
|
AND actor_type = ${page.actorType} AND actor_id = ${page.actorId}`;
|
|
58
62
|
const exportedAtMs = yield* databaseTime;
|
|
@@ -93,14 +97,36 @@ export const exportActor = (page) => tenantSnapshot(page.tenant)(Effect.gen(func
|
|
|
93
97
|
AND actor_id = ${page.actorId}`;
|
|
94
98
|
tableRows += rows.count;
|
|
95
99
|
}
|
|
100
|
+
return { found, stored, intents, jobs, counted: counted, tableRows, exportedAtMs };
|
|
101
|
+
})).pipe(Effect.flatMap((snapshot) => Effect.gen(function* () {
|
|
102
|
+
if (snapshot === undefined)
|
|
103
|
+
return Option.none();
|
|
104
|
+
const { found, stored, intents, jobs, counted, tableRows, exportedAtMs } = snapshot;
|
|
105
|
+
const { routingKey, created } = found;
|
|
106
|
+
const material = found.cold_ref === null
|
|
107
|
+
? undefined
|
|
108
|
+
: yield* Effect.flatMap(ColdTier, (tier) => tier === undefined
|
|
109
|
+
? Effect.die(new Error("Cold export needs coldStorage"))
|
|
110
|
+
: tier
|
|
111
|
+
.fetch({
|
|
112
|
+
key: BigInt(routingKey),
|
|
113
|
+
ref: { tenant: page.tenant, actor: page.actorType, id: page.actorId },
|
|
114
|
+
}, {
|
|
115
|
+
ref: found.cold_ref,
|
|
116
|
+
digest: found.cold_digest,
|
|
117
|
+
version: found.cold_state_version,
|
|
118
|
+
})
|
|
119
|
+
.pipe(Effect.orDie));
|
|
120
|
+
const entries = material === undefined
|
|
121
|
+
? stored.map(({ key, value }) => [key, decodeBytes(value)])
|
|
122
|
+
: material.envelope.state.map(([key, value]) => [key, decodeText(value)]);
|
|
96
123
|
const state = {};
|
|
97
124
|
let stateVersion = 0;
|
|
98
|
-
for (const
|
|
99
|
-
const decoded = decodeBytes(row.value);
|
|
125
|
+
for (const [key, decoded] of entries) {
|
|
100
126
|
if (decoded === null || "undecodable" in decoded)
|
|
101
|
-
return yield* ExportRefused.make({ reason: "undecodable", detail: `state ${
|
|
102
|
-
if (
|
|
103
|
-
state[
|
|
127
|
+
return yield* ExportRefused.make({ reason: "undecodable", detail: `state ${key}` });
|
|
128
|
+
if (key !== VERSION_KEY)
|
|
129
|
+
state[key] = decoded.json;
|
|
104
130
|
else
|
|
105
131
|
stateVersion = Option.getOrElse(decodeVersion(decoded.json), () => 0);
|
|
106
132
|
}
|
|
@@ -131,7 +157,12 @@ export const exportActor = (page) => tenantSnapshot(page.tenant)(Effect.gen(func
|
|
|
131
157
|
key: row.timerKey?.replace(JOB_KEY_PREFIX, "") ?? undefined,
|
|
132
158
|
dueInMs: dueIn(row.dueAtMs),
|
|
133
159
|
}))),
|
|
134
|
-
omitted: {
|
|
160
|
+
omitted: {
|
|
161
|
+
...counted,
|
|
162
|
+
tableRows,
|
|
163
|
+
blobs: counted.blobs +
|
|
164
|
+
(material?.envelope.blobs.filter(({ chunk }) => chunk === 0).length ?? 0),
|
|
165
|
+
},
|
|
135
166
|
};
|
|
136
167
|
return Option.some(seed);
|
|
137
|
-
}));
|
|
168
|
+
})));
|
|
@@ -4,7 +4,7 @@ import { SqlClient } from "effect/sql";
|
|
|
4
4
|
import { ActorError } from "../errors/actor.ts";
|
|
5
5
|
import { type InternalActors } from "./actors.ts";
|
|
6
6
|
import { type Request } from "./request.ts";
|
|
7
|
-
import {
|
|
7
|
+
import type { QueryRegistration, Registration } from "./members.ts";
|
|
8
8
|
import type { Holder } from "./connections/holder.ts";
|
|
9
9
|
export declare const committedReads: ({ registrations, queryRegistrations, services, allow, primary, replica, holder, }: {
|
|
10
10
|
readonly registrations: ReadonlyMap<string, Registration>;
|
package/dist/runtime/queries.js
CHANGED
|
@@ -4,12 +4,13 @@ import { SqlClient, SqlError } from "effect/sql";
|
|
|
4
4
|
import { ActorError, ActorUnavailable, InvalidInput, NotCreated, Timeout } from "../errors/actor.js";
|
|
5
5
|
import {} from "./actors.js";
|
|
6
6
|
import { Outcome } from "./request.js";
|
|
7
|
-
import {} from "./members.js";
|
|
8
7
|
import { watchStream } from "./connections/watch.js";
|
|
9
8
|
import { caughtUp, QueryPool } from "./database/replica.js";
|
|
10
9
|
import { withTenant } from "./database/tenancy.js";
|
|
11
10
|
import { replayEvents } from "./events/replay.js";
|
|
12
11
|
import { decompress, routingKey } from "./storage/codec.js";
|
|
12
|
+
import { ColdTier } from "./storage/cold-tier.js";
|
|
13
|
+
import { ColdRead } from "./turn/blobs.js";
|
|
13
14
|
import { accountsUsage, UsageAccounting } from "./telemetry/usage.js";
|
|
14
15
|
import { decodeResult } from "./workflows/engine.js";
|
|
15
16
|
/**
|
|
@@ -57,15 +58,16 @@ export const committedReads = ({ registrations, queryRegistrations, services, al
|
|
|
57
58
|
const key = routingKey({ ref: request.ref, placement: registration.placement });
|
|
58
59
|
const read = (client) => Effect.gen(function* () {
|
|
59
60
|
const rows = yield* client `
|
|
60
|
-
SELECT event_sequence::text AS head, NULL AS key, NULL::bytea AS value
|
|
61
|
+
SELECT event_sequence::text AS head, NULL AS key, NULL::bytea AS value,
|
|
62
|
+
cold_ref, cold_digest, cold_state_version
|
|
61
63
|
FROM actor_generations
|
|
62
64
|
WHERE routing_key = ${key} AND tenant_id = ${request.ref.tenant}
|
|
63
65
|
AND actor_type = ${request.ref.actor} AND actor_id = ${request.ref.id}
|
|
64
66
|
UNION ALL
|
|
65
|
-
SELECT NULL, key, value
|
|
67
|
+
SELECT NULL, key, value, NULL, NULL, NULL
|
|
66
68
|
FROM actor_state
|
|
67
69
|
WHERE routing_key = ${key} AND tenant_id = ${request.ref.tenant}
|
|
68
|
-
AND actor_type = ${request.ref.actor} AND actor_id = ${request.ref.id}
|
|
70
|
+
AND actor_type = ${request.ref.actor} AND actor_id = ${request.ref.id}`.pipe(withTenant(request.ref.tenant));
|
|
69
71
|
let head;
|
|
70
72
|
const state = [];
|
|
71
73
|
for (const row of rows)
|
|
@@ -76,11 +78,26 @@ export const committedReads = ({ registrations, queryRegistrations, services, al
|
|
|
76
78
|
if (head === undefined)
|
|
77
79
|
state.length = 0;
|
|
78
80
|
const cursor = head ?? "0";
|
|
79
|
-
const
|
|
81
|
+
const pointer = rows.find((row) => row.head !== null);
|
|
82
|
+
const tier = Context.get(services, ColdTier);
|
|
83
|
+
const material = pointer?.cold_ref == null
|
|
84
|
+
? undefined
|
|
85
|
+
: yield* Effect.suspend(() => tier === undefined
|
|
86
|
+
? Effect.die(new Error("Cold query needs coldStorage"))
|
|
87
|
+
: tier
|
|
88
|
+
.fetch({ key, ref: request.ref }, {
|
|
89
|
+
ref: pointer.cold_ref,
|
|
90
|
+
digest: pointer.cold_digest,
|
|
91
|
+
version: pointer.cold_state_version,
|
|
92
|
+
})
|
|
93
|
+
.pipe(Effect.mapError((cause) => ActorError.make({ reason: ActorUnavailable.make({ cause }) }))));
|
|
94
|
+
const outcome = yield* query
|
|
95
|
+
.run(request, material?.envelope.state ?? state, cursor, (tag, after, limit) => replayEvents(request.ref, key, [tag], after, BigInt(cursor), limit).pipe(Effect.catchIf(SqlError.isSqlError, Effect.die), Effect.provideService(SqlClient.SqlClient, client), Effect.provideContext(services)), reads)
|
|
96
|
+
.pipe(Effect.provideService(ColdRead, material), withTenant(request.ref.tenant));
|
|
80
97
|
if (Outcome.guards.Defect(outcome) && SqlError.isSqlError(outcome.cause))
|
|
81
98
|
return yield* outcome.cause;
|
|
82
99
|
return outcome;
|
|
83
|
-
}).pipe(
|
|
100
|
+
}).pipe(Effect.provideService(SqlClient.SqlClient, client));
|
|
84
101
|
const owned = registration.tables.length > 0 || registration.blobs.length > 0;
|
|
85
102
|
const local = owned ? primary : (queryPool ?? primary);
|
|
86
103
|
const outcome = yield* Effect.gen(function* () {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PgliteClient } from "@effect/sql-pglite";
|
|
2
|
+
import { Context, Crypto, Effect, Option } from "effect";
|
|
2
3
|
import { Sharding } from "effect/cluster";
|
|
3
4
|
import { SqlClient, SqlError } from "effect/sql";
|
|
4
5
|
import { isContent } from "../members/blob.js";
|
|
@@ -25,6 +26,29 @@ const ROUTED_SUBSCRIBER_WAIT_MS = 5000;
|
|
|
25
26
|
export const actorRegistration = ({ registrations, queryRegistrations, jobRegistrations, residency, diagnostics, owners, heldTypes, heldType, sweepsWorkflows, checked, services, frameworkClock, content, retryWindowMs, tableRole, adoptionRole, enforcedTypes, writerDeclarations, refreshPayloadWriters, subscriptions, transport, authorize, gate, writable, defectLog, outbox, recorder, }) => {
|
|
26
27
|
/** Concurrent layers can exceed checkout capacity; only a refused checkout is safe to retry here. */
|
|
27
28
|
const startupSql = (effect) => retryPoolRefusal(effect).pipe(Effect.orDie);
|
|
29
|
+
/**
|
|
30
|
+
* A shortened migration history cannot reinterpret older cold objects.
|
|
31
|
+
* PGlite cannot offload, so it needs no transaction that would eagerly start
|
|
32
|
+
* its singleton actors during registration.
|
|
33
|
+
*/
|
|
34
|
+
const checkColdState = Effect.fnUntraced(function* (registration) {
|
|
35
|
+
if (Option.isSome(yield* Effect.serviceOption(PgliteClient.PgliteClient)))
|
|
36
|
+
return;
|
|
37
|
+
const sql = yield* SqlClient.SqlClient;
|
|
38
|
+
yield* sql.withTransaction(Effect.gen(function* () {
|
|
39
|
+
const [record] = yield* sql `SELECT state_version FROM actor_placements
|
|
40
|
+
WHERE actor_type = ${registration.name} FOR UPDATE`;
|
|
41
|
+
const previous = record.state_version;
|
|
42
|
+
const unreadable = yield* sql `SELECT 1 FROM actor_generations
|
|
43
|
+
WHERE actor_type = ${registration.name} AND cold_ref IS NOT NULL
|
|
44
|
+
AND (cold_state_version > ${registration.stateVersion}
|
|
45
|
+
OR (${registration.stateVersion} < ${previous} AND cold_state_version < ${previous})) LIMIT 1`;
|
|
46
|
+
if (unreadable.length > 0)
|
|
47
|
+
return yield* Effect.die(new Error(`Actor ${registration.name} state chain is shortened while cold actors need it; deploy refused`));
|
|
48
|
+
yield* sql `UPDATE actor_placements SET state_version = ${registration.stateVersion}
|
|
49
|
+
WHERE actor_type = ${registration.name}`;
|
|
50
|
+
}));
|
|
51
|
+
});
|
|
28
52
|
/**
|
|
29
53
|
* Refuses a layer that can't read every payload version the database
|
|
30
54
|
* may hold, as a placement or workflow mismatch is refused. `writes`
|
|
@@ -91,6 +115,7 @@ export const actorRegistration = ({ registrations, queryRegistrations, jobRegist
|
|
|
91
115
|
if (registrations.has(registration.name))
|
|
92
116
|
return yield* Effect.die(new Error(`Duplicate actor: ${registration.name}`));
|
|
93
117
|
yield* checkPlacement(registration).pipe(Effect.provideContext(services), startupSql);
|
|
118
|
+
yield* checkColdState(registration).pipe(Effect.provideContext(services), startupSql);
|
|
94
119
|
if (declaresContent(registration)) {
|
|
95
120
|
yield* requireContent(registration.name);
|
|
96
121
|
yield* recordContentTurn(registration).pipe(Effect.provideContext(services), startupSql);
|
|
@@ -159,6 +184,7 @@ export const actorRegistration = ({ registrations, queryRegistrations, jobRegist
|
|
|
159
184
|
if (queryRegistrations.has(registration.name))
|
|
160
185
|
return yield* Effect.die(new Error(`Duplicate query layer: ${registration.name}`));
|
|
161
186
|
yield* checkPlacement(registration).pipe(Effect.provideContext(services), startupSql);
|
|
187
|
+
yield* checkColdState(registration).pipe(Effect.provideContext(services), startupSql);
|
|
162
188
|
if (declaresContent(registration))
|
|
163
189
|
yield* requireContent(registration.name);
|
|
164
190
|
yield* checkTables(registration.name, registration.tables, tableRole, {
|
|
@@ -86,7 +86,7 @@ export declare const Request: Schema.Struct<{
|
|
|
86
86
|
readonly caller: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"User", {
|
|
87
87
|
readonly subject: Schema.NonEmptyString;
|
|
88
88
|
}>, Schema.TaggedStruct<"Anonymous", {}>, Schema.TaggedStruct<"System", {
|
|
89
|
-
readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription"]>;
|
|
89
|
+
readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription", "cold"]>;
|
|
90
90
|
readonly ref: Schema.optional<Schema.Struct<{
|
|
91
91
|
readonly tenant: Schema.NonEmptyString;
|
|
92
92
|
readonly actor: Schema.NonEmptyString;
|
package/dist/runtime/runner.d.ts
CHANGED
|
@@ -71,9 +71,24 @@ export interface SocketRunnerOptions<E> {
|
|
|
71
71
|
readonly shardLockRefreshInterval?: Duration.Input;
|
|
72
72
|
/** Default 1 second. */
|
|
73
73
|
readonly refreshAssignmentsInterval?: Duration.Input;
|
|
74
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Default 15 seconds, bounding activation shutdown during a shard handoff.
|
|
76
|
+
* Must not exceed shardLockExpiration minus the smaller of
|
|
77
|
+
* shardLockRefreshInterval and one third of shardLockExpiration.
|
|
78
|
+
*/
|
|
75
79
|
readonly entityTerminationTimeout?: Duration.Input;
|
|
76
80
|
}
|
|
81
|
+
declare const RunnerConfigurationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
82
|
+
readonly _tag: "RunnerConfigurationError";
|
|
83
|
+
} & Readonly<A>;
|
|
84
|
+
/** Synchronous configuration refusal before runner startup when shutdown can outlast its shard lock. */
|
|
85
|
+
export declare class RunnerConfigurationError extends RunnerConfigurationError_base<{
|
|
86
|
+
readonly entityTerminationTimeoutMs: number;
|
|
87
|
+
readonly shardLockExpirationMs: number;
|
|
88
|
+
readonly effectiveRefreshIntervalMs: number;
|
|
89
|
+
}> {
|
|
90
|
+
get message(): string;
|
|
91
|
+
}
|
|
77
92
|
/**
|
|
78
93
|
* Joins separate processes over TCP sockets. Provide this layer to
|
|
79
94
|
* `Actors.layer`; the framework keeps direct commands, SQL ownership, receipts,
|
|
@@ -82,6 +97,8 @@ export interface SocketRunnerOptions<E> {
|
|
|
82
97
|
* transport unless the network is isolated to this deployment's runners. The
|
|
83
98
|
* transport's own services stay in the runtime, so readiness sees whether a
|
|
84
99
|
* mutual TLS transport can still peer.
|
|
100
|
+
* Throws RunnerConfigurationError synchronously if activation shutdown can
|
|
101
|
+
* outlast the shard lock's remaining lifetime after its effective refresh.
|
|
85
102
|
*/
|
|
86
103
|
export declare const socket: <E>(options: SocketRunnerOptions<E>) => Layer.Layer<RunnerWiring, never, never>;
|
|
87
104
|
/**
|
package/dist/runtime/runner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Duration, Effect, HashRing, Layer, Option, PrimaryKey } from "effect";
|
|
1
|
+
import { Context, Data, Duration, Effect, HashRing, Layer, Option, PrimaryKey } from "effect";
|
|
2
2
|
import { MessageStorage, RunnerAddress, RunnerHealth, Runners, RunnerServer, RunnerStorage, Sharding, ShardingConfig, ShardId, } from "effect/cluster";
|
|
3
3
|
import { RpcSerialization, RpcServer } from "effect/rpc";
|
|
4
4
|
import { SocketServer } from "effect/socket";
|
|
@@ -60,6 +60,12 @@ export const acquiredShards = Effect.gen(function* () {
|
|
|
60
60
|
}).pipe(Effect.timeoutOption("2 seconds"), Effect.map(Option.getOrElse(() => false)), Effect.orElseSucceed(() => false)),
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
|
+
/** Synchronous configuration refusal before runner startup when shutdown can outlast its shard lock. */
|
|
64
|
+
export class RunnerConfigurationError extends Data.TaggedError("RunnerConfigurationError") {
|
|
65
|
+
get message() {
|
|
66
|
+
return `entityTerminationTimeout (${this.entityTerminationTimeoutMs} ms) must not exceed shardLockExpiration minus the effective shardLockRefreshInterval (${this.shardLockExpirationMs - this.effectiveRefreshIntervalMs} ms)`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
63
69
|
const address = (value, advertise) => {
|
|
64
70
|
if (value.host.trim().length === 0 ||
|
|
65
71
|
(advertise && ["0.0.0.0", "::", "*"].includes(value.host)) ||
|
|
@@ -83,21 +89,35 @@ const duration = (value, name, minimum = 1) => {
|
|
|
83
89
|
* transport unless the network is isolated to this deployment's runners. The
|
|
84
90
|
* transport's own services stay in the runtime, so readiness sees whether a
|
|
85
91
|
* mutual TLS transport can still peer.
|
|
92
|
+
* Throws RunnerConfigurationError synchronously if activation shutdown can
|
|
93
|
+
* outlast the shard lock's remaining lifetime after its effective refresh.
|
|
86
94
|
*/
|
|
87
95
|
export const socket = (options) => {
|
|
88
96
|
const shardsPerGroup = options.shardsPerGroup ?? 256;
|
|
89
97
|
if (!Number.isSafeInteger(shardsPerGroup) || shardsPerGroup < 1 || shardsPerGroup > 65536)
|
|
90
98
|
throw new Error("shardsPerGroup must be an integer between 1 and 65536");
|
|
99
|
+
const shardLockExpiration = duration(options.shardLockExpiration ?? "35 seconds", "shardLockExpiration", 3000);
|
|
100
|
+
const shardLockRefreshInterval = duration(options.shardLockRefreshInterval ?? "10 seconds", "shardLockRefreshInterval");
|
|
101
|
+
const entityTerminationTimeout = duration(options.entityTerminationTimeout ?? "15 seconds", "entityTerminationTimeout");
|
|
102
|
+
const shardLockExpirationMs = Duration.toMillis(shardLockExpiration);
|
|
103
|
+
const effectiveRefreshIntervalMs = Math.min(Duration.toMillis(shardLockRefreshInterval), shardLockExpirationMs / 3);
|
|
104
|
+
const entityTerminationTimeoutMs = Duration.toMillis(entityTerminationTimeout);
|
|
105
|
+
if (entityTerminationTimeoutMs > shardLockExpirationMs - effectiveRefreshIntervalMs)
|
|
106
|
+
throw new RunnerConfigurationError({
|
|
107
|
+
entityTerminationTimeoutMs,
|
|
108
|
+
shardLockExpirationMs,
|
|
109
|
+
effectiveRefreshIntervalMs,
|
|
110
|
+
});
|
|
91
111
|
const config = {
|
|
92
112
|
runnerAddress: Option.some(address(options.address, true)),
|
|
93
113
|
runnerListenAddress: Option.some(address(options.listenAddress ?? options.address, false)),
|
|
94
114
|
shardsPerGroup,
|
|
95
115
|
shardLockDisableAdvisory: true,
|
|
96
|
-
shardLockExpiration
|
|
97
|
-
shardLockRefreshInterval
|
|
116
|
+
shardLockExpiration,
|
|
117
|
+
shardLockRefreshInterval,
|
|
98
118
|
refreshAssignmentsInterval: duration(options.refreshAssignmentsInterval ?? "1 second", "refreshAssignmentsInterval"),
|
|
99
119
|
entityMessagePollInterval: "1 second",
|
|
100
|
-
entityTerminationTimeout
|
|
120
|
+
entityTerminationTimeout,
|
|
101
121
|
};
|
|
102
122
|
const sharding = RunnerServer.layer.pipe(Layer.provideMerge(admissionSharding), Layer.provideMerge(Runners.layerRpc), Layer.provide(RpcServer.layerProtocolSocketServer), Layer.provideMerge(options.transport), Layer.provide(RpcSerialization.layerNdjson), Layer.orDie);
|
|
103
123
|
return Layer.succeed(RunnerWiring, {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type S3ClientConfig } from "@aws-sdk/client-s3";
|
|
2
|
+
import { Effect, FileSystem, Path, Stream } from "effect";
|
|
3
|
+
declare const ColdStorageError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
4
|
+
readonly _tag: "ColdStorageError";
|
|
5
|
+
} & Readonly<A>;
|
|
6
|
+
/** A failed object operation, including an unknown upload or deletion outcome. */
|
|
7
|
+
export declare class ColdStorageError extends ColdStorageError_base<{
|
|
8
|
+
readonly cause: unknown;
|
|
9
|
+
}> {
|
|
10
|
+
}
|
|
11
|
+
/** An immutable object's key and creation time, used only as an orphan retention bound. */
|
|
12
|
+
export interface ColdObject {
|
|
13
|
+
readonly key: string;
|
|
14
|
+
readonly createdAtMs: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Private deployment object storage. `put` creates a key once and returns
|
|
18
|
+
* false if it already exists; a retry must verify the existing bytes.
|
|
19
|
+
* `delete` succeeds only when absence is confirmed. Listing is paginated,
|
|
20
|
+
* and failures never mean an empty object or a successful deletion.
|
|
21
|
+
*/
|
|
22
|
+
export interface ColdStorage {
|
|
23
|
+
readonly get: (key: string) => Effect.Effect<Uint8Array, ColdStorageError>;
|
|
24
|
+
readonly put: (key: string, bytes: Uint8Array) => Effect.Effect<boolean, ColdStorageError>;
|
|
25
|
+
readonly delete: (key: string) => Effect.Effect<void, ColdStorageError>;
|
|
26
|
+
readonly list: (prefix: string) => Stream.Stream<ColdObject, ColdStorageError>;
|
|
27
|
+
}
|
|
28
|
+
/** A deterministic create-only store for local tests; copies bytes at both boundaries. */
|
|
29
|
+
declare const memory: () => ColdStorage;
|
|
30
|
+
/** S3-compatible credentials, endpoint, bucket, and deployment encryption settings. */
|
|
31
|
+
export interface S3ColdStorageOptions extends S3ClientConfig {
|
|
32
|
+
readonly bucket: string;
|
|
33
|
+
/** Omitted, use the bucket's configured encryption. */
|
|
34
|
+
readonly encryption?: "AES256" | "aws:kms";
|
|
35
|
+
readonly encryptionKey?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Object-storage adapters for runtime-only cold state, never tenant content. */
|
|
38
|
+
export declare const ColdStorage: {
|
|
39
|
+
memory: typeof memory;
|
|
40
|
+
filesystem: (directory: string) => Effect.Effect<{
|
|
41
|
+
get: (key: string) => Effect.Effect<Uint8Array<ArrayBufferLike>, ColdStorageError, never>;
|
|
42
|
+
put: (key: string, bytes: Uint8Array<ArrayBufferLike>) => Effect.Effect<boolean, ColdStorageError, never>;
|
|
43
|
+
delete: (key: string) => Effect.Effect<void, ColdStorageError, never>;
|
|
44
|
+
list: (prefix: string) => Stream.Stream<ColdObject, ColdStorageError, never>;
|
|
45
|
+
}, never, FileSystem.FileSystem | Path.Path>;
|
|
46
|
+
s3: (args_0: S3ColdStorageOptions) => Effect.Effect<{
|
|
47
|
+
get: (key: string) => Effect.Effect<Uint8Array<ArrayBufferLike>, ColdStorageError, never>;
|
|
48
|
+
put: (key: string, bytes: Uint8Array<ArrayBufferLike>) => Effect.Effect<boolean, ColdStorageError, never>;
|
|
49
|
+
delete: (key: string) => Effect.Effect<void, ColdStorageError, never>;
|
|
50
|
+
list: (prefix: string) => Stream.Stream<{
|
|
51
|
+
key: string;
|
|
52
|
+
createdAtMs: number;
|
|
53
|
+
}, ColdStorageError, never>;
|
|
54
|
+
}, never, import("effect/Scope").Scope>;
|
|
55
|
+
};
|
|
56
|
+
export {};
|