@bjornpagen/bumbledb 1.3.1 → 2.0.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/COOKBOOK.md +153 -77
- package/README.md +107 -41
- package/dist/alternatives.d.ts +5 -6
- package/dist/alternatives.d.ts.map +1 -1
- package/dist/alternatives.js +5 -6
- package/dist/alternatives.js.map +1 -1
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +85 -0
- package/dist/bin.js.map +1 -0
- package/dist/capacity.d.ts +14 -17
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js +7 -7
- package/dist/capacity.js.map +1 -1
- package/dist/changes.d.ts +32 -40
- package/dist/changes.d.ts.map +1 -1
- package/dist/changes.js +86 -104
- package/dist/changes.js.map +1 -1
- package/dist/closed.d.ts +2 -2
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +5 -5
- package/dist/closed.js.map +1 -1
- package/dist/codec.d.ts +13 -27
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +20 -21
- package/dist/codec.js.map +1 -1
- package/dist/compile.d.ts +26 -50
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +75 -64
- package/dist/compile.js.map +1 -1
- package/dist/database/database.d.ts +98 -0
- package/dist/database/database.d.ts.map +1 -0
- package/dist/database/database.js +249 -0
- package/dist/database/database.js.map +1 -0
- package/dist/database/driver.d.ts +40 -0
- package/dist/database/driver.d.ts.map +1 -0
- package/dist/database/driver.js +68 -0
- package/dist/database/driver.js.map +1 -0
- package/dist/database/fs.d.ts +12 -0
- package/dist/database/fs.d.ts.map +1 -0
- package/dist/database/fs.js +120 -0
- package/dist/database/fs.js.map +1 -0
- package/dist/database/generate.d.ts +26 -0
- package/dist/database/generate.d.ts.map +1 -0
- package/dist/database/generate.js +102 -0
- package/dist/database/generate.js.map +1 -0
- package/dist/database/hosted.d.ts +32 -0
- package/dist/database/hosted.d.ts.map +1 -0
- package/dist/database/hosted.js +44 -0
- package/dist/database/hosted.js.map +1 -0
- package/dist/database/io.d.ts +94 -0
- package/dist/database/io.d.ts.map +1 -0
- package/dist/database/io.js +35 -0
- package/dist/database/io.js.map +1 -0
- package/dist/database/mem.d.ts +26 -0
- package/dist/database/mem.d.ts.map +1 -0
- package/dist/database/mem.js +67 -0
- package/dist/database/mem.js.map +1 -0
- package/dist/database/migration.d.ts +55 -0
- package/dist/database/migration.d.ts.map +1 -0
- package/dist/database/migration.js +46 -0
- package/dist/database/migration.js.map +1 -0
- package/dist/database/s3.d.ts +36 -0
- package/dist/database/s3.d.ts.map +1 -0
- package/dist/database/s3.js +141 -0
- package/dist/database/s3.js.map +1 -0
- package/dist/db.d.ts +44 -128
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +163 -287
- package/dist/db.js.map +1 -1
- package/dist/engine.d.ts +7 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +2 -0
- package/dist/engine.js.map +1 -0
- package/dist/errors.d.ts +152 -46
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +107 -19
- package/dist/errors.js.map +1 -1
- package/dist/face.d.ts +5 -5
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +6 -6
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +4 -5
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +3 -3
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +60 -57
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -28
- package/dist/index.js.map +1 -1
- package/dist/judgment.d.ts +1 -1
- package/dist/judgment.js +1 -1
- package/dist/law.d.ts +16 -68
- package/dist/law.d.ts.map +1 -1
- package/dist/law.js +3 -3
- package/dist/law.js.map +1 -1
- package/dist/lower.d.ts +3 -3
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +35 -39
- package/dist/lower.js.map +1 -1
- package/dist/native/addon.d.ts +23 -0
- package/dist/native/addon.d.ts.map +1 -0
- package/dist/native/addon.js +15 -0
- package/dist/native/addon.js.map +1 -0
- package/dist/native/binding.d.ts +996 -0
- package/dist/native/json.d.ts +14 -0
- package/dist/native/json.d.ts.map +1 -0
- package/dist/native/json.js +73 -0
- package/dist/native/json.js.map +1 -0
- package/dist/native/load.d.ts +9 -0
- package/dist/native/load.d.ts.map +1 -0
- package/dist/native/load.js +30 -0
- package/dist/native/load.js.map +1 -0
- package/dist/native/op.d.ts +29 -0
- package/dist/native/op.d.ts.map +1 -0
- package/dist/native/op.js +50 -0
- package/dist/native/op.js.map +1 -0
- package/dist/query/atom.d.ts +13 -14
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +2 -2
- package/dist/query/atom.js.map +1 -1
- package/dist/query/compute.d.ts +4 -4
- package/dist/query/compute.d.ts.map +1 -1
- package/dist/query/compute.js +4 -4
- package/dist/query/compute.js.map +1 -1
- package/dist/query/find.d.ts +8 -8
- package/dist/query/find.d.ts.map +1 -1
- package/dist/query/find.js +1 -1
- package/dist/query/lower.d.ts +22 -17
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +230 -343
- package/dist/query/lower.js.map +1 -1
- package/dist/query/run.d.ts +5 -5
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +32 -15
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +20 -28
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +7 -7
- package/dist/query/scope.js.map +1 -1
- package/dist/query/segments.d.ts +2 -2
- package/dist/query/segments.d.ts.map +1 -1
- package/dist/query/segments.js +3 -3
- package/dist/query/segments.js.map +1 -1
- package/dist/relation.d.ts +1 -1
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +4 -4
- package/dist/relation.js.map +1 -1
- package/dist/result.d.ts +15 -22
- package/dist/result.d.ts.map +1 -1
- package/dist/result.js +30 -39
- package/dist/result.js.map +1 -1
- package/dist/rows.d.ts +9 -33
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +8 -12
- package/dist/rows.js.map +1 -1
- package/dist/runtime.d.ts +17 -20
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +41 -205
- package/dist/runtime.js.map +1 -1
- package/dist/scalar.d.ts +4 -4
- package/dist/scalar.d.ts.map +1 -1
- package/dist/scalar.js +24 -8
- package/dist/scalar.js.map +1 -1
- package/dist/schema.d.ts +7 -18
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +18 -228
- package/dist/schema.js.map +1 -1
- package/dist/selection.d.ts +4 -4
- package/dist/selection.d.ts.map +1 -1
- package/dist/selection.js +5 -5
- package/dist/selection.js.map +1 -1
- package/dist/shape.d.ts +5 -5
- package/dist/shape.d.ts.map +1 -1
- package/dist/spec.d.ts +7 -79
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +3 -3
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +10 -10
- package/dist/statements.js.map +1 -1
- package/dist/uuid.d.ts +1 -1
- package/dist/uuid.d.ts.map +1 -1
- package/dist/uuid.js +1 -1
- package/dist/uuid.js.map +1 -1
- package/dist/values.d.ts +2 -2
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +2 -2
- package/dist/values.js.map +1 -1
- package/package.json +18 -18
- package/src/alternatives.ts +7 -8
- package/src/bin.ts +82 -0
- package/src/capacity.ts +20 -23
- package/src/changes.ts +134 -191
- package/src/closed.ts +5 -5
- package/src/codec.ts +35 -49
- package/src/compile.ts +91 -113
- package/src/database/database.ts +413 -0
- package/src/database/driver.ts +134 -0
- package/src/database/fs.ts +119 -0
- package/src/database/generate.ts +116 -0
- package/src/database/hosted.ts +102 -0
- package/src/database/io.ts +134 -0
- package/src/database/mem.ts +113 -0
- package/src/database/migration.ts +107 -0
- package/src/database/s3.ts +231 -0
- package/src/db.ts +290 -547
- package/src/engine.ts +14 -0
- package/src/errors.ts +118 -39
- package/src/face.ts +9 -9
- package/src/fields.ts +6 -7
- package/src/index.ts +92 -105
- package/src/judgment.ts +1 -1
- package/src/law.ts +18 -70
- package/src/lower.ts +61 -72
- package/src/native/addon.ts +54 -0
- package/src/native/binding.d.ts +996 -0
- package/src/native/json.ts +100 -0
- package/src/native/load.ts +31 -0
- package/src/native/op.ts +73 -0
- package/src/query/atom.ts +15 -16
- package/src/query/compute.ts +7 -7
- package/src/query/find.ts +8 -8
- package/src/query/lower.ts +270 -392
- package/src/query/run.ts +42 -21
- package/src/query/scope.ts +29 -37
- package/src/query/segments.ts +4 -4
- package/src/relation.ts +4 -4
- package/src/result.ts +53 -80
- package/src/rows.ts +18 -46
- package/src/runtime.ts +50 -277
- package/src/scalar.ts +32 -12
- package/src/schema.ts +23 -284
- package/src/selection.ts +6 -6
- package/src/shape.ts +5 -5
- package/src/spec.ts +7 -85
- package/src/statements.ts +11 -11
- package/src/uuid.ts +1 -1
- package/src/values.ts +4 -4
- package/dist/close.d.ts +0 -24
- package/dist/close.d.ts.map +0 -1
- package/dist/close.js +0 -34
- package/dist/close.js.map +0 -1
- package/dist/db-native.d.ts +0 -189
- package/dist/db-native.d.ts.map +0 -1
- package/dist/db-native.js +0 -13
- package/dist/db-native.js.map +0 -1
- package/dist/internal/log.d.ts +0 -26
- package/dist/internal/log.d.ts.map +0 -1
- package/dist/internal/log.js +0 -12
- package/dist/internal/log.js.map +0 -1
- package/dist/native.d.ts +0 -562
- package/dist/native.d.ts.map +0 -1
- package/dist/native.js +0 -123
- package/dist/native.js.map +0 -1
- package/dist/query/description.d.ts +0 -41
- package/dist/query/description.d.ts.map +0 -1
- package/dist/query/description.js +0 -311
- package/dist/query/description.js.map +0 -1
- package/dist/query/parse-ir.d.ts +0 -10
- package/dist/query/parse-ir.d.ts.map +0 -1
- package/dist/query/parse-ir.js +0 -280
- package/dist/query/parse-ir.js.map +0 -1
- package/dist/runtime-codes.d.ts +0 -2
- package/dist/runtime-codes.d.ts.map +0 -1
- package/dist/runtime-codes.js +0 -19
- package/dist/runtime-codes.js.map +0 -1
- package/dist/runtime-errors.d.ts +0 -138
- package/dist/runtime-errors.d.ts.map +0 -1
- package/dist/runtime-errors.js +0 -111
- package/dist/runtime-errors.js.map +0 -1
- package/dist/runtime-native.d.ts +0 -136
- package/dist/runtime-native.d.ts.map +0 -1
- package/dist/runtime-native.js +0 -5
- package/dist/runtime-native.js.map +0 -1
- package/dist/schema-file.d.ts +0 -7
- package/dist/schema-file.d.ts.map +0 -1
- package/dist/schema-file.js +0 -24
- package/dist/schema-file.js.map +0 -1
- package/pack-provenance.json +0 -7
- package/src/close.ts +0 -53
- package/src/db-native.ts +0 -235
- package/src/internal/log.ts +0 -46
- package/src/native.ts +0 -736
- package/src/query/description.ts +0 -424
- package/src/query/parse-ir.ts +0 -293
- package/src/runtime-codes.ts +0 -18
- package/src/runtime-errors.ts +0 -122
- package/src/runtime-native.ts +0 -181
- package/src/schema-file.ts +0 -38
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drives one sans-IO log machine. Requests and store responses go through one mailbox, so the
|
|
3
|
+
* machine steps strictly one input at a time; every `IoRequest` a step emits runs concurrently
|
|
4
|
+
* (bounded) and comes back as another input. Each request carries a ticket that settles exactly
|
|
5
|
+
* once, and closing the driver settles every ticket still open.
|
|
6
|
+
*/
|
|
7
|
+
import type { Scope } from "effect"
|
|
8
|
+
import { Deferred, Effect, Fiber, FiberSet, Queue, Semaphore } from "effect"
|
|
9
|
+
import { DbError } from "../errors.ts"
|
|
10
|
+
import type { ExecutorOptions, IoRequest, IoResponse, ObjectStore } from "./io.ts"
|
|
11
|
+
import { defaultExecutorOptions, execute } from "./io.ts"
|
|
12
|
+
|
|
13
|
+
interface Step<Settled> {
|
|
14
|
+
readonly io: readonly IoRequest[]
|
|
15
|
+
readonly done: ReadonlyArray<{ readonly ticket: bigint; readonly settled: Settled }>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The machine as the driver sees it. The driver never calls it concurrently. */
|
|
19
|
+
interface MachinePort<Request, Settled> {
|
|
20
|
+
readonly request: (ticket: bigint, request: Request) => Effect.Effect<Step<Settled>, DbError>
|
|
21
|
+
readonly respond: (response: IoResponse) => Effect.Effect<Step<Settled>, DbError>
|
|
22
|
+
/** Stops the machine; its step settles every open ticket. */
|
|
23
|
+
readonly close: Effect.Effect<Step<Settled>, DbError>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface DriverOptions {
|
|
27
|
+
/** Store calls in flight at once. */
|
|
28
|
+
readonly concurrency: number
|
|
29
|
+
readonly executor: ExecutorOptions
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Driver<Request, Settled> {
|
|
33
|
+
/** Submits one request and waits for its settlement. */
|
|
34
|
+
readonly run: (request: Request) => Effect.Effect<Settled, DbError>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Message<Request> =
|
|
38
|
+
| { readonly _tag: "Request"; readonly ticket: bigint; readonly request: Request }
|
|
39
|
+
| { readonly _tag: "Response"; readonly response: IoResponse }
|
|
40
|
+
| { readonly _tag: "Close" }
|
|
41
|
+
|
|
42
|
+
const defaultDriverOptions: DriverOptions = { concurrency: 32, executor: defaultExecutorOptions }
|
|
43
|
+
|
|
44
|
+
/** Starts a driver owned by the current scope; closing the scope closes the machine. */
|
|
45
|
+
const make = Effect.fnUntraced(function* <Request, Settled>(
|
|
46
|
+
port: MachinePort<Request, Settled>,
|
|
47
|
+
store: ObjectStore,
|
|
48
|
+
options: DriverOptions = defaultDriverOptions
|
|
49
|
+
): Effect.fn.Return<Driver<Request, Settled>, never, Scope.Scope> {
|
|
50
|
+
const mailbox = yield* Queue.unbounded<Message<Request>>()
|
|
51
|
+
const permits = yield* Semaphore.make(options.concurrency)
|
|
52
|
+
const io = yield* FiberSet.make()
|
|
53
|
+
const tickets = new Map<bigint, Deferred.Deferred<Settled, DbError>>()
|
|
54
|
+
let nextTicket = 0n
|
|
55
|
+
const closed = new DbError({ operation: "Database", reason: { _tag: "ClosedHandle" } })
|
|
56
|
+
|
|
57
|
+
const apply = (step: Step<Settled>) =>
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
for (const { ticket, settled } of step.done) {
|
|
60
|
+
const waiter = tickets.get(ticket)
|
|
61
|
+
tickets.delete(ticket)
|
|
62
|
+
if (waiter !== undefined) yield* Deferred.succeed(waiter, settled)
|
|
63
|
+
}
|
|
64
|
+
for (const request of step.io) {
|
|
65
|
+
yield* FiberSet.run(
|
|
66
|
+
io,
|
|
67
|
+
Semaphore.withPermit(permits, execute(store, request, options.executor)).pipe(
|
|
68
|
+
Effect.flatMap((response) => Queue.offer(mailbox, { _tag: "Response", response }))
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
let stopped: DbError | undefined
|
|
75
|
+
|
|
76
|
+
/** Ends the driver: later runs fail with `error`, and open tickets fail with it now. */
|
|
77
|
+
const stop = (error: DbError) =>
|
|
78
|
+
Effect.suspend(() => {
|
|
79
|
+
stopped ??= error
|
|
80
|
+
const open = [...tickets.values()]
|
|
81
|
+
tickets.clear()
|
|
82
|
+
return Effect.forEach(open, (waiter) => Deferred.fail(waiter, error), { discard: true })
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const stepFor = (message: Message<Request>) => {
|
|
86
|
+
switch (message._tag) {
|
|
87
|
+
case "Request":
|
|
88
|
+
return port.request(message.ticket, message.request)
|
|
89
|
+
case "Response":
|
|
90
|
+
return port.respond(message.response)
|
|
91
|
+
case "Close":
|
|
92
|
+
return port.close
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const loop: Effect.Effect<void> = Effect.gen(function* () {
|
|
97
|
+
while (true) {
|
|
98
|
+
const message = yield* Queue.take(mailbox)
|
|
99
|
+
const step = stepFor(message)
|
|
100
|
+
const failed = yield* step.pipe(
|
|
101
|
+
Effect.flatMap(apply),
|
|
102
|
+
Effect.as(false),
|
|
103
|
+
Effect.catch((error) => Effect.as(stop(error), true))
|
|
104
|
+
)
|
|
105
|
+
if (failed || message._tag === "Close") return yield* stop(closed)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const machine = yield* Effect.forkScoped(loop)
|
|
110
|
+
yield* Effect.addFinalizer(() =>
|
|
111
|
+
Queue.offer(mailbox, { _tag: "Close" }).pipe(
|
|
112
|
+
Effect.andThen(Fiber.await(machine)),
|
|
113
|
+
Effect.andThen(FiberSet.clear(io))
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
run: (request: Request) =>
|
|
119
|
+
Effect.suspend(() => {
|
|
120
|
+
if (stopped !== undefined) return Effect.fail(stopped)
|
|
121
|
+
const waiter = Deferred.makeUnsafe<Settled, DbError>()
|
|
122
|
+
const ticket = nextTicket
|
|
123
|
+
nextTicket += 1n
|
|
124
|
+
tickets.set(ticket, waiter)
|
|
125
|
+
Queue.offerUnsafe(mailbox, { _tag: "Request", ticket, request })
|
|
126
|
+
return Deferred.await(waiter)
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const Driver = Object.freeze({ make, defaultOptions: defaultDriverOptions })
|
|
132
|
+
|
|
133
|
+
export type { DriverOptions, MachinePort, Step }
|
|
134
|
+
export { Driver }
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import * as fs from "node:fs/promises"
|
|
3
|
+
import * as path from "node:path"
|
|
4
|
+
import { Effect } from "effect"
|
|
5
|
+
import { DbError } from "../errors.ts"
|
|
6
|
+
import type { Body, Bucket, Created, Deleted, Fetched, Listed, ObjectStore, Reply, Target } from "./io.ts"
|
|
7
|
+
|
|
8
|
+
function code(cause: unknown): string | undefined {
|
|
9
|
+
return typeof cause === "object" && cause !== null && "code" in cause && typeof cause.code === "string"
|
|
10
|
+
? cause.code
|
|
11
|
+
: undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function attempt<A>(operation: string, run: () => Promise<A>): Effect.Effect<A, DbError> {
|
|
15
|
+
return Effect.tryPromise({
|
|
16
|
+
try: run,
|
|
17
|
+
catch: (cause) => new DbError({ operation, reason: { _tag: "Io", kind: code(cause) ?? "Unknown" } })
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function syncDirectory(directory: string): Promise<void> {
|
|
22
|
+
const handle = await fs.open(directory, "r")
|
|
23
|
+
try {
|
|
24
|
+
await handle.sync()
|
|
25
|
+
} finally {
|
|
26
|
+
await handle.close()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function writeDurably(file: string, body: Body): Promise<void> {
|
|
31
|
+
if (body._tag === "File") await fs.copyFile(body.path, file, fs.constants.COPYFILE_EXCL)
|
|
32
|
+
const handle = await fs.open(file, body._tag === "File" ? "r+" : "wx")
|
|
33
|
+
try {
|
|
34
|
+
if (body._tag === "Bytes") await handle.writeFile(body.bytes)
|
|
35
|
+
await handle.sync()
|
|
36
|
+
} finally {
|
|
37
|
+
await handle.close()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A store in a local directory, one file per key under `root`. A create writes a temporary file,
|
|
43
|
+
* fsyncs it, hard-links it into place (the link fails if the key exists) and fsyncs the directory,
|
|
44
|
+
* so a created key is durable and never torn. The store clock is the local clock.
|
|
45
|
+
*/
|
|
46
|
+
function make(root: string): ObjectStore {
|
|
47
|
+
const file = (key: string) => path.join(root, ...key.split("/"))
|
|
48
|
+
const now = () => BigInt(Date.now())
|
|
49
|
+
return {
|
|
50
|
+
get: (_bucket: Bucket, key: string, target: Target) =>
|
|
51
|
+
attempt("FsStore.get", async (): Promise<Reply<Fetched>> => {
|
|
52
|
+
try {
|
|
53
|
+
if (target._tag === "Memory") {
|
|
54
|
+
const [bytes, stat] = await Promise.all([fs.readFile(file(key)), fs.stat(file(key), { bigint: true })])
|
|
55
|
+
return {
|
|
56
|
+
date: now(),
|
|
57
|
+
result: { _tag: "Body", bytes: new Uint8Array(bytes), lastModified: stat.mtimeMs }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const stat = await fs.stat(file(key), { bigint: true })
|
|
61
|
+
await fs.copyFile(file(key), target.path)
|
|
62
|
+
return { date: now(), result: { _tag: "Saved", lastModified: stat.mtimeMs } }
|
|
63
|
+
} catch (cause) {
|
|
64
|
+
if (code(cause) === "ENOENT") return { date: now(), result: { _tag: "Missing" } }
|
|
65
|
+
throw cause
|
|
66
|
+
}
|
|
67
|
+
}),
|
|
68
|
+
putIfAbsent: (_bucket: Bucket, key: string, body: Body) =>
|
|
69
|
+
attempt("FsStore.putIfAbsent", async (): Promise<Reply<Created>> => {
|
|
70
|
+
const target = file(key)
|
|
71
|
+
const directory = path.dirname(target)
|
|
72
|
+
await fs.mkdir(directory, { recursive: true })
|
|
73
|
+
const temporary = path.join(directory, `.tmp-${randomUUID()}`)
|
|
74
|
+
try {
|
|
75
|
+
await writeDurably(temporary, body)
|
|
76
|
+
try {
|
|
77
|
+
await fs.link(temporary, target)
|
|
78
|
+
} catch (cause) {
|
|
79
|
+
if (code(cause) === "EEXIST") return { date: now(), result: { _tag: "Occupied" } }
|
|
80
|
+
throw cause
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
await fs.rm(temporary, { force: true })
|
|
84
|
+
}
|
|
85
|
+
await syncDirectory(directory)
|
|
86
|
+
return { date: now(), result: { _tag: "Created" } }
|
|
87
|
+
}),
|
|
88
|
+
list: (prefix: string, startAfter: string | null, maxKeys: number) =>
|
|
89
|
+
attempt("FsStore.list", async (): Promise<Reply<Listed>> => {
|
|
90
|
+
const directory = path.posix.dirname(`${prefix}_`)
|
|
91
|
+
let names: string[]
|
|
92
|
+
try {
|
|
93
|
+
names = await fs.readdir(path.join(root, ...directory.split("/")))
|
|
94
|
+
} catch (cause) {
|
|
95
|
+
if (code(cause) === "ENOENT") return { date: now(), result: { _tag: "Keys", keys: [] } }
|
|
96
|
+
throw cause
|
|
97
|
+
}
|
|
98
|
+
const keys = names
|
|
99
|
+
.filter((name) => !name.startsWith(".tmp-"))
|
|
100
|
+
.map((name) => (directory === "." ? name : `${directory}/${name}`))
|
|
101
|
+
.filter((key) => key.startsWith(prefix) && (startAfter === null || key > startAfter))
|
|
102
|
+
.sort()
|
|
103
|
+
.slice(0, maxKeys)
|
|
104
|
+
return { date: now(), result: { _tag: "Keys", keys } }
|
|
105
|
+
}),
|
|
106
|
+
delete: (key: string) =>
|
|
107
|
+
attempt("FsStore.delete", async (): Promise<Reply<Deleted>> => {
|
|
108
|
+
await fs.rm(file(key), { force: true })
|
|
109
|
+
await syncDirectory(path.dirname(file(key))).catch((cause) => {
|
|
110
|
+
if (code(cause) !== "ENOENT") throw cause
|
|
111
|
+
})
|
|
112
|
+
return { date: now(), result: { _tag: "Deleted" } }
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const FsStore = Object.freeze({ make })
|
|
118
|
+
|
|
119
|
+
export { FsStore }
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The migrations directory: one `NNNN_name/` per migration holding the schema it migrates to
|
|
3
|
+
* (`schema.json`, the engine's spec, and `schema.ts`, the same schema as SDK code) and its
|
|
4
|
+
* `migration.ts`; `index.ts` bundles them in order. A migration's hash covers its id and its
|
|
5
|
+
* `schema.json`, so editing either after it ran is caught by `check`.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from "node:crypto"
|
|
8
|
+
import * as fs from "node:fs"
|
|
9
|
+
import * as path from "node:path"
|
|
10
|
+
import { compiledOf } from "../compile.ts"
|
|
11
|
+
import { AuthoringError } from "../errors.ts"
|
|
12
|
+
import { lower } from "../lower.ts"
|
|
13
|
+
import { addon } from "../native/addon.ts"
|
|
14
|
+
import type { AnySchema } from "../schema.ts"
|
|
15
|
+
import { schemaDescriptor } from "../schema.ts"
|
|
16
|
+
|
|
17
|
+
const DIRECTORY = /^([0-9]{4})_([A-Za-z0-9_]+)$/
|
|
18
|
+
|
|
19
|
+
/** The `schema.json` text of a schema: its lowered spec, formatted for review. */
|
|
20
|
+
function specText(theory: AnySchema): string {
|
|
21
|
+
return `${JSON.stringify(lower(schemaDescriptor(theory)), null, "\t")}\n`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A migration's hash: sha256 over its id and its `schema.json` text. */
|
|
25
|
+
function migrationHash(id: string, spec: string): string {
|
|
26
|
+
return createHash("sha256").update(`${id}\n${spec}`).digest("hex")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The migration directories under `root`, in order. */
|
|
30
|
+
function migrationIds(root: string): string[] {
|
|
31
|
+
if (!fs.existsSync(root)) return []
|
|
32
|
+
return fs
|
|
33
|
+
.readdirSync(root, { withFileTypes: true })
|
|
34
|
+
.filter((entry) => entry.isDirectory() && DIRECTORY.test(entry.name))
|
|
35
|
+
.map((entry) => entry.name)
|
|
36
|
+
.sort()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function indexText(ids: readonly string[]): string {
|
|
40
|
+
const imports = ids.map((id) => `import m${id.slice(0, 4)} from "./${id}/migration.ts"`)
|
|
41
|
+
const list = ids.map((id) => `m${id.slice(0, 4)}`).join(", ")
|
|
42
|
+
return `${imports.join("\n")}\n\nexport const migrations = [${list}] as const\n`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function migrationText(id: string, hash: string, previous: string | undefined): string {
|
|
46
|
+
const lines = ['import { Migration } from "@bjornpagen/bumbledb"']
|
|
47
|
+
if (previous !== undefined) lines.push(`import { schema as from } from "../${previous}/schema.ts"`)
|
|
48
|
+
lines.push('import { schema as to } from "./schema.ts"', "")
|
|
49
|
+
lines.push(
|
|
50
|
+
previous === undefined
|
|
51
|
+
? `export default Migration.make({ id: "${id}", hash: "${hash}", to })`
|
|
52
|
+
: `export default Migration.make({ id: "${id}", hash: "${hash}", from, to })`
|
|
53
|
+
)
|
|
54
|
+
return `${lines.join("\n")}\n`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type Generated = { readonly _tag: "Unchanged" } | { readonly _tag: "Created"; readonly id: string }
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Adds a migration to `schema` under `root` when it differs from the last migration's schema, and
|
|
61
|
+
* rewrites `index.ts`.
|
|
62
|
+
*/
|
|
63
|
+
function generate(root: string, theory: AnySchema, name: string): Generated {
|
|
64
|
+
if (!/^[A-Za-z0-9_]+$/.test(name))
|
|
65
|
+
throw new AuthoringError({ message: `migration name ${name}: letters, digits and _` })
|
|
66
|
+
const ids = migrationIds(root)
|
|
67
|
+
const spec = specText(theory)
|
|
68
|
+
const previous = ids.at(-1)
|
|
69
|
+
if (previous !== undefined && fs.readFileSync(path.join(root, previous, "schema.json"), "utf8") === spec) {
|
|
70
|
+
return { _tag: "Unchanged" }
|
|
71
|
+
}
|
|
72
|
+
const bindings = addon.schemaBindings(compiledOf(schemaDescriptor(theory)).handle)
|
|
73
|
+
if (bindings._tag === "Unrepresentable") {
|
|
74
|
+
throw new AuthoringError({ message: `schema ${theory.name}: ${bindings.coordinate}: ${bindings.reason}` })
|
|
75
|
+
}
|
|
76
|
+
const number = previous === undefined ? 1 : Number(previous.slice(0, 4)) + 1
|
|
77
|
+
const id = `${number.toString().padStart(4, "0")}_${name}`
|
|
78
|
+
const directory = path.join(root, id)
|
|
79
|
+
fs.mkdirSync(directory, { recursive: true })
|
|
80
|
+
fs.writeFileSync(path.join(directory, "schema.json"), spec)
|
|
81
|
+
fs.writeFileSync(path.join(directory, "schema.ts"), bindings.source)
|
|
82
|
+
fs.writeFileSync(path.join(directory, "migration.ts"), migrationText(id, migrationHash(id, spec), previous))
|
|
83
|
+
fs.writeFileSync(path.join(root, "index.ts"), indexText([...ids, id]))
|
|
84
|
+
return { _tag: "Created", id }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Everything `check` found wrong; empty when the directory is consistent with `schema`. */
|
|
88
|
+
function check(
|
|
89
|
+
root: string,
|
|
90
|
+
theory: AnySchema,
|
|
91
|
+
bundled: readonly { readonly id: string; readonly hash: string }[]
|
|
92
|
+
): string[] {
|
|
93
|
+
const problems: string[] = []
|
|
94
|
+
const ids = migrationIds(root)
|
|
95
|
+
if (ids.length === 0) return [`${root}: no migrations; run bumbledb generate`]
|
|
96
|
+
if (fs.readFileSync(path.join(root, "index.ts"), "utf8") !== indexText(ids)) {
|
|
97
|
+
problems.push(`${root}/index.ts does not list the migration directories in order; run bumbledb generate`)
|
|
98
|
+
}
|
|
99
|
+
ids.forEach((id, index) => {
|
|
100
|
+
const spec = fs.readFileSync(path.join(root, id, "schema.json"), "utf8")
|
|
101
|
+
const migration = bundled[index]
|
|
102
|
+
if (migration === undefined || migration.id !== id) {
|
|
103
|
+
problems.push(`${id}: not bundled at position ${index + 1}`)
|
|
104
|
+
} else if (migration.hash !== migrationHash(id, spec)) {
|
|
105
|
+
problems.push(`${id}: its hash does not match its schema.json; a migration is never edited after it runs`)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
const last = ids.at(-1)
|
|
109
|
+
if (last !== undefined && fs.readFileSync(path.join(root, last, "schema.json"), "utf8") !== specText(theory)) {
|
|
110
|
+
problems.push(`the schema differs from ${last}; run bumbledb generate`)
|
|
111
|
+
}
|
|
112
|
+
return problems
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type { Generated }
|
|
116
|
+
export { check, generate, migrationHash, migrationIds, specText }
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The addon's hosted machine as a driver port. A hosted step is never cancelled: its output carries
|
|
3
|
+
* requests the machine then waits on, so every step runs to completion even when its caller is
|
|
4
|
+
* interrupted.
|
|
5
|
+
*/
|
|
6
|
+
import { Effect } from "effect"
|
|
7
|
+
import type { DbError } from "../errors.ts"
|
|
8
|
+
import type { ChangesRef, OperationRef } from "../native/addon.ts"
|
|
9
|
+
import { addon } from "../native/addon.ts"
|
|
10
|
+
import type {
|
|
11
|
+
CopyPair,
|
|
12
|
+
ExternalObject,
|
|
13
|
+
HeadOut,
|
|
14
|
+
HostedHandle,
|
|
15
|
+
IoOutcomeIn,
|
|
16
|
+
SettledOut,
|
|
17
|
+
StepOut
|
|
18
|
+
} from "../native/binding.d.ts"
|
|
19
|
+
import { call } from "../native/op.ts"
|
|
20
|
+
import type { MachinePort, Step } from "./driver.ts"
|
|
21
|
+
import type { IoResponse, IoResult } from "./io.ts"
|
|
22
|
+
|
|
23
|
+
type HostedRef = ExternalObject<HostedHandle>
|
|
24
|
+
|
|
25
|
+
/** One machine request. `request` ids are 32 hex digits; `revision` present is an exact-revision precondition. */
|
|
26
|
+
type HostedRequest =
|
|
27
|
+
| { readonly _tag: "Open" }
|
|
28
|
+
| { readonly _tag: "Sync" }
|
|
29
|
+
| { readonly _tag: "Resolve"; readonly request: string }
|
|
30
|
+
| { readonly _tag: "Freeze"; readonly leaseMillis: bigint }
|
|
31
|
+
| {
|
|
32
|
+
readonly _tag: "Submit"
|
|
33
|
+
readonly request: string
|
|
34
|
+
readonly revision: bigint | null
|
|
35
|
+
readonly changes: ChangesRef
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
readonly _tag: "Migrate"
|
|
39
|
+
readonly step: number
|
|
40
|
+
readonly base: bigint
|
|
41
|
+
readonly copy: readonly CopyPair[]
|
|
42
|
+
readonly rows: ChangesRef
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function outcomeIn(result: Exclude<IoResult, { readonly _tag: "Body" }>): IoOutcomeIn {
|
|
46
|
+
return result._tag === "Keys" ? { _tag: "Keys", keys: [...result.keys] } : result
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A port over one hosted machine; `onHead` sees every head a step reports. */
|
|
50
|
+
function hostedPort(hosted: HostedRef, onHead: (head: HeadOut) => void): MachinePort<HostedRequest, SettledOut> {
|
|
51
|
+
const step = (
|
|
52
|
+
operation: string,
|
|
53
|
+
start: (done: () => void) => OperationRef
|
|
54
|
+
): Effect.Effect<Step<SettledOut>, DbError> =>
|
|
55
|
+
call(operation, start, addon.hostedStepTake).pipe(
|
|
56
|
+
Effect.tap((out: StepOut) =>
|
|
57
|
+
Effect.sync(() => {
|
|
58
|
+
if (out.head !== undefined) onHead(out.head)
|
|
59
|
+
})
|
|
60
|
+
),
|
|
61
|
+
Effect.uninterruptible
|
|
62
|
+
)
|
|
63
|
+
return {
|
|
64
|
+
request: (ticket, request) => {
|
|
65
|
+
switch (request._tag) {
|
|
66
|
+
case "Open":
|
|
67
|
+
case "Sync":
|
|
68
|
+
return step(`Database.${request._tag}`, (done) =>
|
|
69
|
+
addon.hostedStep(hosted, { _tag: request._tag, ticket }, done)
|
|
70
|
+
)
|
|
71
|
+
case "Resolve":
|
|
72
|
+
return step("Database.resolve", (done) =>
|
|
73
|
+
addon.hostedStep(hosted, { _tag: "Resolve", ticket, request: request.request }, done)
|
|
74
|
+
)
|
|
75
|
+
case "Freeze":
|
|
76
|
+
return step("Database.freeze", (done) =>
|
|
77
|
+
addon.hostedStep(hosted, { _tag: "Freeze", ticket, leaseMillis: request.leaseMillis }, done)
|
|
78
|
+
)
|
|
79
|
+
case "Submit":
|
|
80
|
+
return step("Database.submit", (done) =>
|
|
81
|
+
addon.hostedSubmit(hosted, ticket, request.request, request.revision, request.changes, done)
|
|
82
|
+
)
|
|
83
|
+
case "Migrate":
|
|
84
|
+
return step("Database.migrate", (done) =>
|
|
85
|
+
addon.hostedMigrate(hosted, ticket, request.step, request.base, [...request.copy], request.rows, done)
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
respond: (response: IoResponse) => {
|
|
90
|
+
const { id, date, result } = response
|
|
91
|
+
return result._tag === "Body"
|
|
92
|
+
? step("Database.respond", (done) =>
|
|
93
|
+
addon.hostedRespondBody(hosted, id, date, result.lastModified, result.bytes, done)
|
|
94
|
+
)
|
|
95
|
+
: step("Database.respond", (done) => addon.hostedRespond(hosted, id, date, outcomeIn(result), done))
|
|
96
|
+
},
|
|
97
|
+
close: step("Database.close", (done) => addon.hostedStep(hosted, { _tag: "Close" }, done))
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type { HostedRef, HostedRequest }
|
|
102
|
+
export { hostedPort }
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The object-store side of the hosted log. The log machine emits `IoRequest`s as data; this module
|
|
3
|
+
* runs them against an `ObjectStore` and turns every outcome into an `IoResponse`. Reads are
|
|
4
|
+
* idempotent and retried here; a create-only PUT is never retried here, because the machine
|
|
5
|
+
* resolves an ambiguous PUT by reading the key back.
|
|
6
|
+
*/
|
|
7
|
+
import type { Duration } from "effect"
|
|
8
|
+
import { Effect, Schedule } from "effect"
|
|
9
|
+
import { DbError } from "../errors.ts"
|
|
10
|
+
import type { BucketOut, IoRequestOut } from "../native/binding.d.ts"
|
|
11
|
+
|
|
12
|
+
/** `Log` is the commit log (`log/`, an S3 Express directory bucket); `Checkpoints` holds `ckpt/` and `mig/`. */
|
|
13
|
+
type Bucket = BucketOut
|
|
14
|
+
|
|
15
|
+
/** One request from the machine. `key` is relative to the database prefix. */
|
|
16
|
+
type IoRequest = IoRequestOut
|
|
17
|
+
|
|
18
|
+
/** Unix milliseconds on the store's clock. */
|
|
19
|
+
type Millis = bigint
|
|
20
|
+
|
|
21
|
+
/** Where a GET delivers the object: returned in memory, or written to a local file. */
|
|
22
|
+
type Target = { readonly _tag: "Memory" } | { readonly _tag: "File"; readonly path: string }
|
|
23
|
+
|
|
24
|
+
/** What a create-only PUT uploads: bytes in memory, or a local file. */
|
|
25
|
+
type Body = { readonly _tag: "Bytes"; readonly bytes: Uint8Array } | { readonly _tag: "File"; readonly path: string }
|
|
26
|
+
|
|
27
|
+
type Fetched =
|
|
28
|
+
| { readonly _tag: "Body"; readonly bytes: Uint8Array; readonly lastModified: Millis }
|
|
29
|
+
| { readonly _tag: "Saved"; readonly lastModified: Millis }
|
|
30
|
+
| { readonly _tag: "Missing" }
|
|
31
|
+
|
|
32
|
+
/** `Occupied`: the key exists, or, for a log object, another conditional write to it was in progress. */
|
|
33
|
+
type Created = { readonly _tag: "Created" } | { readonly _tag: "Occupied" }
|
|
34
|
+
|
|
35
|
+
type Listed = { readonly _tag: "Keys"; readonly keys: readonly string[] }
|
|
36
|
+
|
|
37
|
+
type Deleted = { readonly _tag: "Deleted" }
|
|
38
|
+
|
|
39
|
+
/** `Failed` covers everything a store could not answer definitively: 5xx, timeouts, transport. */
|
|
40
|
+
type IoResult = Fetched | Created | Listed | Deleted | { readonly _tag: "Failed" }
|
|
41
|
+
|
|
42
|
+
interface IoResponse {
|
|
43
|
+
readonly id: bigint
|
|
44
|
+
/** The store's `Date` header, when it sent one. */
|
|
45
|
+
readonly date: Millis | null
|
|
46
|
+
readonly result: IoResult
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A store answer plus the store's clock reading. */
|
|
50
|
+
interface Reply<R> {
|
|
51
|
+
readonly date: Millis | null
|
|
52
|
+
readonly result: R
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The four verbs the log needs. Keys arrive relative; a store owns its own prefix. LIST and DELETE
|
|
57
|
+
* exist only for `Checkpoints`: LIST order on an Express bucket is not lexicographic, and a `log/`
|
|
58
|
+
* key must never disappear.
|
|
59
|
+
*/
|
|
60
|
+
interface ObjectStore {
|
|
61
|
+
readonly get: (bucket: Bucket, key: string, target: Target) => Effect.Effect<Reply<Fetched>, DbError>
|
|
62
|
+
readonly putIfAbsent: (bucket: Bucket, key: string, body: Body) => Effect.Effect<Reply<Created>, DbError>
|
|
63
|
+
/** Checkpoint keys under `prefix` after `startAfter`, in lexicographic order. */
|
|
64
|
+
readonly list: (prefix: string, startAfter: string | null, maxKeys: number) => Effect.Effect<Reply<Listed>, DbError>
|
|
65
|
+
/** Deletes one checkpoint key; deleting a missing key succeeds. */
|
|
66
|
+
readonly delete: (key: string) => Effect.Effect<Reply<Deleted>, DbError>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface ExecutorOptions {
|
|
70
|
+
/** Bound on one store call; a call that exceeds it counts as `Failed` (or is retried, for a read). */
|
|
71
|
+
readonly timeout: Duration.Input
|
|
72
|
+
/** Retry policy for GET, LIST and DELETE. */
|
|
73
|
+
readonly readRetry: Schedule.Schedule<unknown, unknown>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const defaultExecutorOptions: ExecutorOptions = {
|
|
77
|
+
timeout: "10 seconds",
|
|
78
|
+
readRetry: Schedule.exponential("20 millis").pipe(Schedule.jittered, Schedule.upTo({ times: 4 }))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function refused(request: IoRequest): Effect.Effect<never, DbError> {
|
|
82
|
+
return Effect.fail(new DbError({ operation: `ObjectStore.${request.op._tag}`, reason: { _tag: "InvalidArgument" } }))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function dispatch(store: ObjectStore, request: IoRequest): Effect.Effect<Reply<IoResult>, DbError> {
|
|
86
|
+
const { bucket, key, op } = request
|
|
87
|
+
switch (op._tag) {
|
|
88
|
+
case "GetMemory":
|
|
89
|
+
return store.get(bucket, key, { _tag: "Memory" })
|
|
90
|
+
case "GetFile":
|
|
91
|
+
return store.get(bucket, key, { _tag: "File", path: op.path })
|
|
92
|
+
case "PutBytes":
|
|
93
|
+
return store.putIfAbsent(bucket, key, { _tag: "Bytes", bytes: op.bytes })
|
|
94
|
+
case "PutFile":
|
|
95
|
+
return store.putIfAbsent(bucket, key, { _tag: "File", path: op.path })
|
|
96
|
+
case "List":
|
|
97
|
+
return bucket === "Checkpoints" ? store.list(key, op.startAfter ?? null, op.maxKeys) : refused(request)
|
|
98
|
+
case "Delete":
|
|
99
|
+
return bucket === "Checkpoints" ? store.delete(key) : refused(request)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Runs one request to an `IoResponse`; it never fails. */
|
|
104
|
+
function execute(
|
|
105
|
+
store: ObjectStore,
|
|
106
|
+
request: IoRequest,
|
|
107
|
+
options: ExecutorOptions = defaultExecutorOptions
|
|
108
|
+
): Effect.Effect<IoResponse> {
|
|
109
|
+
const once = Effect.timeout(dispatch(store, request), options.timeout)
|
|
110
|
+
const write = request.op._tag === "PutBytes" || request.op._tag === "PutFile"
|
|
111
|
+
const attempts = write ? once : Effect.retry(once, options.readRetry)
|
|
112
|
+
return attempts.pipe(
|
|
113
|
+
Effect.map((reply): IoResponse => ({ id: request.id, date: reply.date, result: reply.result })),
|
|
114
|
+
Effect.catch(() => Effect.succeed<IoResponse>({ id: request.id, date: null, result: { _tag: "Failed" } }))
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type {
|
|
119
|
+
Body,
|
|
120
|
+
Bucket,
|
|
121
|
+
Created,
|
|
122
|
+
Deleted,
|
|
123
|
+
ExecutorOptions,
|
|
124
|
+
Fetched,
|
|
125
|
+
IoRequest,
|
|
126
|
+
IoResponse,
|
|
127
|
+
IoResult,
|
|
128
|
+
Listed,
|
|
129
|
+
Millis,
|
|
130
|
+
ObjectStore,
|
|
131
|
+
Reply,
|
|
132
|
+
Target
|
|
133
|
+
}
|
|
134
|
+
export { defaultExecutorOptions, execute }
|