solid-objects 0.12.1 → 0.13.1

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.
Files changed (115) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/README.md +284 -950
  3. package/dist/actor.d.ts +12 -0
  4. package/dist/actor.d.ts.map +1 -1
  5. package/dist/actor.js +25 -2
  6. package/dist/actor.js.map +1 -1
  7. package/dist/broadcast-worker.d.ts +2 -0
  8. package/dist/broadcast-worker.d.ts.map +1 -1
  9. package/dist/broadcast-worker.js +25 -5
  10. package/dist/broadcast-worker.js.map +1 -1
  11. package/dist/browser/components.d.ts.map +1 -1
  12. package/dist/browser/components.js +4 -1
  13. package/dist/browser/components.js.map +1 -1
  14. package/dist/browser/index.d.ts +1 -0
  15. package/dist/browser/index.d.ts.map +1 -1
  16. package/dist/browser/index.js +8 -0
  17. package/dist/browser/index.js.map +1 -1
  18. package/dist/cli.d.ts.map +1 -1
  19. package/dist/cli.js +13 -1
  20. package/dist/cli.js.map +1 -1
  21. package/dist/configuration.d.ts +3 -0
  22. package/dist/configuration.d.ts.map +1 -1
  23. package/dist/configuration.js +3 -0
  24. package/dist/configuration.js.map +1 -1
  25. package/dist/doctor.d.ts.map +1 -1
  26. package/dist/doctor.js +12 -3
  27. package/dist/doctor.js.map +1 -1
  28. package/dist/effect-worker.d.ts +2 -0
  29. package/dist/effect-worker.d.ts.map +1 -1
  30. package/dist/effect-worker.js +25 -5
  31. package/dist/effect-worker.js.map +1 -1
  32. package/dist/examples/sqlite-quickstart.js +83 -0
  33. package/dist/examples/sqlite-quickstart.js.map +1 -0
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/polling-backoff.d.ts +23 -0
  39. package/dist/polling-backoff.d.ts.map +1 -0
  40. package/dist/polling-backoff.js +33 -0
  41. package/dist/polling-backoff.js.map +1 -0
  42. package/dist/records.d.ts +1 -0
  43. package/dist/records.d.ts.map +1 -1
  44. package/dist/reminder-scheduler.d.ts +2 -0
  45. package/dist/reminder-scheduler.d.ts.map +1 -1
  46. package/dist/reminder-scheduler.js +25 -5
  47. package/dist/reminder-scheduler.js.map +1 -1
  48. package/dist/repository.d.ts +5 -1
  49. package/dist/repository.d.ts.map +1 -1
  50. package/dist/repository.js +15 -4
  51. package/dist/repository.js.map +1 -1
  52. package/dist/runtime.d.ts +4 -0
  53. package/dist/runtime.d.ts.map +1 -1
  54. package/dist/runtime.js +71 -6
  55. package/dist/runtime.js.map +1 -1
  56. package/dist/schema.d.ts.map +1 -1
  57. package/dist/schema.js +15 -4
  58. package/dist/schema.js.map +1 -1
  59. package/dist/version.d.ts +1 -1
  60. package/dist/version.js +1 -1
  61. package/dist/wake-up/postgresql.d.ts.map +1 -1
  62. package/dist/wake-up/postgresql.js +12 -11
  63. package/dist/wake-up/postgresql.js.map +1 -1
  64. package/dist/wake-up.d.ts +1 -1
  65. package/dist/wake-up.d.ts.map +1 -1
  66. package/dist/wake-up.js +11 -10
  67. package/dist/wake-up.js.map +1 -1
  68. package/dist/web/assets.d.ts +4 -0
  69. package/dist/web/assets.d.ts.map +1 -0
  70. package/dist/web/assets.js +11 -0
  71. package/dist/web/assets.js.map +1 -0
  72. package/dist/web/index.d.ts +28 -0
  73. package/dist/web/index.d.ts.map +1 -0
  74. package/dist/web/index.js +642 -0
  75. package/dist/web/index.js.map +1 -0
  76. package/dist/web/node.d.ts +3 -0
  77. package/dist/web/node.d.ts.map +1 -0
  78. package/dist/web/node.js +109 -0
  79. package/dist/web/node.js.map +1 -0
  80. package/dist/web/render.d.ts +64 -0
  81. package/dist/web/render.d.ts.map +1 -0
  82. package/dist/web/render.js +331 -0
  83. package/dist/web/render.js.map +1 -0
  84. package/dist/web/store.d.ts +65 -0
  85. package/dist/web/store.d.ts.map +1 -0
  86. package/dist/web/store.js +303 -0
  87. package/dist/web/store.js.map +1 -0
  88. package/dist/web/types.d.ts +82 -0
  89. package/dist/web/types.d.ts.map +1 -0
  90. package/dist/web/types.js +2 -0
  91. package/dist/web/types.js.map +1 -0
  92. package/dist/worker.d.ts +2 -0
  93. package/dist/worker.d.ts.map +1 -1
  94. package/dist/worker.js +25 -5
  95. package/dist/worker.js.map +1 -1
  96. package/docs/api.md +70 -4
  97. package/docs/architecture.md +20 -11
  98. package/docs/authorization.md +12 -3
  99. package/docs/benchmarks.md +163 -0
  100. package/docs/browser-protocol.md +15 -8
  101. package/docs/comparisons.md +36 -0
  102. package/docs/configuration.md +14 -3
  103. package/docs/correctness.md +23 -0
  104. package/docs/dashboard.md +196 -0
  105. package/docs/fit.md +58 -0
  106. package/docs/operations.md +25 -7
  107. package/docs/parity.md +37 -34
  108. package/docs/releasing.md +6 -4
  109. package/docs/state-and-lifecycle.md +10 -0
  110. package/docs/support.md +39 -0
  111. package/examples/failure-recovery/actor.ts +51 -0
  112. package/examples/failure-recovery/demo.ts +233 -0
  113. package/examples/failure-recovery/worker.ts +46 -0
  114. package/examples/sqlite-quickstart.ts +109 -0
  115. package/package.json +26 -4
@@ -0,0 +1,233 @@
1
+ import assert from "node:assert/strict"
2
+ import { existsSync } from "node:fs"
3
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"
4
+ import { tmpdir } from "node:os"
5
+ import { join } from "node:path"
6
+ import { fileURLToPath } from "node:url"
7
+ import { fork, type ChildProcess } from "node:child_process"
8
+ import { createRuntime, type ActorReference, type MessageReference } from "solid-objects"
9
+ import { sqlite } from "solid-objects/database/sqlite"
10
+ import { RecoveryCounter } from "./actor.ts"
11
+
12
+ interface WorkerMessage {
13
+ event: string
14
+ attempt?: number
15
+ processed?: number
16
+ }
17
+
18
+ interface SerializationEvent {
19
+ event: "start" | "finish"
20
+ messageId: string
21
+ at: number
22
+ }
23
+
24
+ interface ExternalEffectEvent {
25
+ messageId: string
26
+ attempt: number
27
+ processId: number
28
+ }
29
+
30
+ const directory = await mkdtemp(join(tmpdir(), "solid-objects-recovery-"))
31
+ const databasePath = join(directory, "state.sqlite3")
32
+ const runtime = createRuntime({
33
+ database: sqlite({ path: databasePath, timeoutMilliseconds: 2_000, lockRetryAttempts: 20 }),
34
+ leaseDurationMilliseconds: 250,
35
+ leaseRenewalIntervalMilliseconds: 50,
36
+ processHeartbeatIntervalMilliseconds: 75,
37
+ processAliveThresholdMilliseconds: 300,
38
+ workerCount: 1,
39
+ effectWorkerCount: 0,
40
+ reminderSchedulerCount: 0,
41
+ retentionIntervalMilliseconds: 0,
42
+ deadProcessCleanupIntervalMilliseconds: 0,
43
+ authorizeMessage: () => true,
44
+ authorizeQuery: () => true,
45
+ authorizeAdministration: () => true,
46
+ })
47
+
48
+ try {
49
+ runtime.register(RecoveryCounter)
50
+ await runtime.install()
51
+ const serialization = await proveSerialization()
52
+ const crash = await proveCrashRecovery()
53
+ const fencing = await proveFencing()
54
+ process.stdout.write(`${JSON.stringify({ serialization, crash, fencing }, null, 2)}\n`)
55
+ } finally {
56
+ await runtime.close()
57
+ await rm(directory, { recursive: true })
58
+ }
59
+
60
+ assert.equal(existsSync(directory), false)
61
+
62
+ async function proveSerialization(): Promise<{ finalState: number; overlap: false }> {
63
+ const controlDirectory = join(directory, "serialization")
64
+ await mkdir(controlDirectory)
65
+ const reference = runtime.ref(RecoveryCounter, "serialized")
66
+ const messages = await Promise.all([
67
+ reference.send.serialize({ controlDirectory }),
68
+ reference.send.serialize({ controlDirectory }),
69
+ ])
70
+ const workers = [spawnWorker(), spawnWorker()]
71
+ await Promise.all(workers.map(({ finished }) => finished))
72
+ await Promise.all(messages.map((message) => message.result()))
73
+ const events = await readJsonLines(
74
+ join(controlDirectory, "serialization.jsonl"),
75
+ parseSerializationEvent,
76
+ )
77
+ assert.equal(events.length, 4)
78
+ const starts = events.filter((event) => event.event === "start")
79
+ const finishes = events.filter((event) => event.event === "finish")
80
+ assert.equal(starts.length, 2)
81
+ assert.equal(finishes.length, 2)
82
+ assert(Number(starts[1]?.at) >= Number(finishes[0]?.at))
83
+ const snapshot = await reference.snapshot()
84
+ assert.equal(snapshot.count, 2)
85
+ return { finalState: snapshot.count, overlap: false }
86
+ }
87
+
88
+ async function proveCrashRecovery(): Promise<{
89
+ attempts: number
90
+ finalState: number
91
+ repeatedEffects: number
92
+ }> {
93
+ const controlDirectory = join(directory, "crash")
94
+ await mkdir(controlDirectory)
95
+ const reference = runtime.ref(RecoveryCounter, "crash")
96
+ const message = await reference.send.recover({ controlDirectory })
97
+ const firstWorker = spawnWorker()
98
+ await firstWorker.waitFor((entry) => entry.event === "operation.started" && entry.attempt === 1)
99
+ firstWorker.child.kill("SIGKILL")
100
+ await firstWorker.finished.catch(() => undefined)
101
+ await wait(350)
102
+ const recoveryWorker = spawnWorker()
103
+ await recoveryWorker.finished
104
+ await message.result()
105
+ return recoveryResult({ reference, message, controlDirectory })
106
+ }
107
+
108
+ async function proveFencing(): Promise<{
109
+ attempts: number
110
+ finalState: number
111
+ repeatedEffects: number
112
+ }> {
113
+ const controlDirectory = join(directory, "fencing")
114
+ await mkdir(controlDirectory)
115
+ const reference = runtime.ref(RecoveryCounter, "fencing")
116
+ const message = await reference.send.recover({ controlDirectory })
117
+ const staleWorker = spawnWorker()
118
+ await staleWorker.waitFor((entry) => entry.event === "operation.started" && entry.attempt === 1)
119
+ staleWorker.child.kill("SIGSTOP")
120
+ await wait(350)
121
+ const recoveryWorker = spawnWorker()
122
+ await recoveryWorker.finished
123
+ await message.result()
124
+ staleWorker.child.kill("SIGCONT")
125
+ await writeFile(join(controlDirectory, "release-first-attempt"), "")
126
+ await staleWorker.waitFor((entry) => entry.event === "solid_objects.activation.lost")
127
+ await staleWorker.finished
128
+ return recoveryResult({ reference, message, controlDirectory })
129
+ }
130
+
131
+ async function recoveryResult(options: {
132
+ reference: ActorReference<RecoveryCounter>
133
+ message: MessageReference
134
+ controlDirectory: string
135
+ }): Promise<{ attempts: number; finalState: number; repeatedEffects: number }> {
136
+ const stored = await runtime.repository.findMessage(options.message.id)
137
+ const attempts = Number(stored?.attempt_count)
138
+ const snapshot = await options.reference.snapshot()
139
+ const effects = await readJsonLines(
140
+ join(options.controlDirectory, "external-effects.jsonl"),
141
+ parseExternalEffectEvent,
142
+ )
143
+ assert.equal(attempts, 2)
144
+ assert.equal(snapshot.count, 1)
145
+ assert.equal(effects.length, 2)
146
+ return { attempts, finalState: snapshot.count, repeatedEffects: effects.length }
147
+ }
148
+
149
+ function spawnWorker(): {
150
+ child: ChildProcess
151
+ finished: Promise<void>
152
+ waitFor(predicate: (message: WorkerMessage) => boolean): Promise<WorkerMessage>
153
+ } {
154
+ const child = fork(fileURLToPath(new URL("./worker.ts", import.meta.url)), [databasePath], {
155
+ cwd: fileURLToPath(new URL("../..", import.meta.url)),
156
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
157
+ })
158
+ const messages: WorkerMessage[] = []
159
+ const listeners = new Set<(message: WorkerMessage) => void>()
160
+ let stderr = ""
161
+ child.stderr?.on("data", (chunk) => {
162
+ stderr += chunk
163
+ })
164
+ child.on("message", (message: WorkerMessage) => {
165
+ messages.push(message)
166
+ for (const listener of listeners) listener(message)
167
+ })
168
+ const finished = new Promise<void>((resolvePromise, reject) => {
169
+ child.once("error", reject)
170
+ child.once("exit", (code, signal) => {
171
+ if (code === 0) {
172
+ resolvePromise()
173
+ return
174
+ }
175
+ reject(new Error(`worker exited with code ${code} and signal ${signal}\n${stderr}`))
176
+ })
177
+ })
178
+ return {
179
+ child,
180
+ finished,
181
+ waitFor: (predicate) => {
182
+ const existing = messages.find(predicate)
183
+ if (existing) return Promise.resolve(existing)
184
+ return new Promise((resolvePromise) => {
185
+ const listener = (message: WorkerMessage) => {
186
+ if (!predicate(message)) return
187
+ listeners.delete(listener)
188
+ resolvePromise(message)
189
+ }
190
+ listeners.add(listener)
191
+ })
192
+ },
193
+ }
194
+ }
195
+
196
+ async function readJsonLines<Value>(
197
+ path: string,
198
+ parse: (line: string) => Value,
199
+ ): Promise<Value[]> {
200
+ return (await readFile(path, "utf8")).trim().split("\n").filter(Boolean).map(parse)
201
+ }
202
+
203
+ function parseSerializationEvent(line: string): SerializationEvent {
204
+ const event = JSON.parse(line) as Partial<SerializationEvent>
205
+ if (
206
+ (event.event !== "start" && event.event !== "finish") ||
207
+ typeof event.messageId !== "string" ||
208
+ typeof event.at !== "number"
209
+ ) {
210
+ throw new TypeError("invalid serialization event")
211
+ }
212
+ return { event: event.event, messageId: event.messageId, at: event.at }
213
+ }
214
+
215
+ function parseExternalEffectEvent(line: string): ExternalEffectEvent {
216
+ const event = JSON.parse(line) as Partial<ExternalEffectEvent>
217
+ if (
218
+ typeof event.messageId !== "string" ||
219
+ typeof event.attempt !== "number" ||
220
+ typeof event.processId !== "number"
221
+ ) {
222
+ throw new TypeError("invalid external effect event")
223
+ }
224
+ return {
225
+ messageId: event.messageId,
226
+ attempt: event.attempt,
227
+ processId: event.processId,
228
+ }
229
+ }
230
+
231
+ async function wait(milliseconds: number): Promise<void> {
232
+ await new Promise((resolve) => setTimeout(resolve, milliseconds))
233
+ }
@@ -0,0 +1,46 @@
1
+ import { createRuntime } from "solid-objects"
2
+ import { sqlite } from "solid-objects/database/sqlite"
3
+ import { RecoveryCounter } from "./actor.ts"
4
+
5
+ const databasePath = requiredArgument(2)
6
+ const runtime = createRuntime({
7
+ database: sqlite({ path: databasePath, timeoutMilliseconds: 2_000, lockRetryAttempts: 20 }),
8
+ pollingIntervalMilliseconds: 10,
9
+ leaseDurationMilliseconds: 250,
10
+ leaseRenewalIntervalMilliseconds: 50,
11
+ processHeartbeatIntervalMilliseconds: 75,
12
+ processAliveThresholdMilliseconds: 300,
13
+ workerCount: 1,
14
+ effectWorkerCount: 0,
15
+ reminderSchedulerCount: 0,
16
+ retentionIntervalMilliseconds: 0,
17
+ deadProcessCleanupIntervalMilliseconds: 0,
18
+ authorizeMessage: () => true,
19
+ authorizeQuery: () => true,
20
+ authorizeAdministration: () => true,
21
+ instrumentation: ({ name, attributes }) => {
22
+ process.send?.({ event: name, attributes })
23
+ },
24
+ })
25
+
26
+ runtime.register(RecoveryCounter)
27
+ await runtime.install()
28
+ const worker = runtime.worker()
29
+
30
+ try {
31
+ let processed = 0
32
+ for (let attempt = 0; attempt < 200 && processed === 0; attempt += 1) {
33
+ processed = await worker.runOnce({ activationRetention: "release" })
34
+ if (processed === 0) await new Promise((resolve) => setTimeout(resolve, 10))
35
+ }
36
+ process.send?.({ event: "worker.finished", processed })
37
+ } finally {
38
+ await worker.stop()
39
+ await runtime.close()
40
+ }
41
+
42
+ function requiredArgument(index: number): string {
43
+ const value = process.argv[index]
44
+ if (!value) throw new TypeError(`argument ${index - 1} is required`)
45
+ return value
46
+ }
@@ -0,0 +1,109 @@
1
+ import assert from "node:assert/strict"
2
+ import { existsSync } from "node:fs"
3
+ import { mkdtemp, rm } from "node:fs/promises"
4
+ import { tmpdir } from "node:os"
5
+ import { join, resolve } from "node:path"
6
+ import { fileURLToPath } from "node:url"
7
+ import { Actor, createRuntime } from "solid-objects"
8
+ import { sqlite } from "solid-objects/database/sqlite"
9
+
10
+ class Counter extends Actor {
11
+ static override readonly actorType = "QuickstartCounter"
12
+
13
+ count = 0
14
+
15
+ increment(): number {
16
+ this.count += 1
17
+ return this.count
18
+ }
19
+
20
+ async pause({ milliseconds }: { milliseconds: number }): Promise<{
21
+ startedAt: number
22
+ finishedAt: number
23
+ }> {
24
+ const startedAt = performance.now()
25
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds))
26
+ return { startedAt, finishedAt: performance.now() }
27
+ }
28
+ }
29
+
30
+ export async function runQuickstart(
31
+ options: {
32
+ signal?: AbortSignal
33
+ write?: (value: string) => void
34
+ } = {},
35
+ ): Promise<void> {
36
+ const directory = await mkdtemp(join(tmpdir(), "solid-objects-quickstart-"))
37
+ const databasePath = join(directory, "state.sqlite3")
38
+ const runtime = createRuntime({
39
+ database: sqlite({ path: databasePath }),
40
+ workerCount: 2,
41
+ effectWorkerCount: 0,
42
+ reminderSchedulerCount: 0,
43
+ retentionIntervalMilliseconds: 0,
44
+ deadProcessCleanupIntervalMilliseconds: 0,
45
+ authorizeMessage: () => true,
46
+ authorizeQuery: () => true,
47
+ })
48
+ const shutdown = new AbortController()
49
+ const abort = () => shutdown.abort(options.signal?.reason)
50
+ options.signal?.addEventListener("abort", abort, { once: true })
51
+ let running: Promise<void> | undefined
52
+ let sameIdentityFinalState = 0
53
+ let independentIdentitiesOverlapped = false
54
+
55
+ try {
56
+ runtime.register(Counter)
57
+ await runtime.install()
58
+ running = runtime.run(shutdown.signal)
59
+
60
+ const counter = runtime.ref(Counter, "room-1")
61
+ const results = await Promise.all(Array.from({ length: 25 }, () => counter.increment()))
62
+ assert.deepEqual(
63
+ [...results].sort((left, right) => left - right),
64
+ Array.from({ length: 25 }, (_value, index) => index + 1),
65
+ )
66
+ sameIdentityFinalState = await counter.count
67
+ assert.equal(sameIdentityFinalState, 25)
68
+
69
+ const pauses = await Promise.all([
70
+ runtime.ref(Counter, "room-2").send.pause({ milliseconds: 100 }),
71
+ runtime.ref(Counter, "room-3").send.pause({ milliseconds: 100 }),
72
+ ])
73
+ const windows = await Promise.all(
74
+ pauses.map((message) => message.wait({ timeoutMilliseconds: 5_000 })),
75
+ )
76
+ const firstWindow = windows[0]
77
+ const secondWindow = windows[1]
78
+ if (!firstWindow || !secondWindow) throw new Error("two execution windows are required")
79
+ independentIdentitiesOverlapped =
80
+ firstWindow.startedAt < secondWindow.finishedAt &&
81
+ secondWindow.startedAt < firstWindow.finishedAt
82
+ assert.equal(independentIdentitiesOverlapped, true)
83
+ } finally {
84
+ shutdown.abort()
85
+ await running
86
+ await runtime.close()
87
+ options.signal?.removeEventListener("abort", abort)
88
+ await rm(directory, { recursive: true })
89
+ }
90
+
91
+ assert.equal(existsSync(directory), false)
92
+ const write = options.write ?? ((value: string) => process.stdout.write(value))
93
+ write(
94
+ `${JSON.stringify(
95
+ {
96
+ sameIdentityCalls: 25,
97
+ sameIdentityFinalState,
98
+ independentIdentitiesOverlapped,
99
+ temporaryStateRemoved: true,
100
+ },
101
+ null,
102
+ 2,
103
+ )}\n`,
104
+ )
105
+ }
106
+
107
+ if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
108
+ await runQuickstart()
109
+ }
package/package.json CHANGED
@@ -1,10 +1,23 @@
1
1
  {
2
2
  "name": "solid-objects",
3
- "version": "0.12.1",
4
- "description": "Durable, addressable objects for Node.js",
3
+ "version": "0.13.1",
4
+ "description": "Race-free realtime state per application identity, backed by your SQL database",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Lucas Carlson",
8
+ "keywords": [
9
+ "concurrency",
10
+ "durable-state",
11
+ "mysql",
12
+ "nodejs",
13
+ "postgresql",
14
+ "realtime",
15
+ "sqlite",
16
+ "state-management",
17
+ "typescript",
18
+ "actors",
19
+ "durable-objects"
20
+ ],
8
21
  "repository": {
9
22
  "type": "git",
10
23
  "url": "git+https://github.com/cardmagic/solid-objects-js.git"
@@ -19,6 +32,7 @@
19
32
  "files": [
20
33
  "dist",
21
34
  "docs",
35
+ "examples",
22
36
  "README.md",
23
37
  "CHANGELOG.md",
24
38
  "MIT-LICENSE"
@@ -47,11 +61,15 @@
47
61
  "./browser": {
48
62
  "types": "./dist/browser/index.d.ts",
49
63
  "import": "./dist/browser/index.js"
64
+ },
65
+ "./web": {
66
+ "types": "./dist/web/index.d.ts",
67
+ "import": "./dist/web/index.js"
50
68
  }
51
69
  },
52
70
  "scripts": {
53
- "build": "pnpm run clean && tsc -p tsconfig.build.json && node scripts/prepare-executable.mjs",
54
- "check": "pnpm run check:parameters && pnpm run check:documentation && tsc -p tsconfig.json --noEmit",
71
+ "build": "pnpm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.quickstart-build.json && node scripts/prepare-executable.mjs",
72
+ "check": "pnpm run check:parameters && pnpm run check:documentation && tsc -p tsconfig.json --noEmit && tsc -p tsconfig.examples.json --noEmit",
55
73
  "check:documentation": "node scripts/check-documentation.mjs",
56
74
  "check:parameters": "node scripts/check-parameter-style.mjs",
57
75
  "clean": "node scripts/clean.mjs",
@@ -62,8 +80,12 @@
62
80
  "test:coverage": "vitest run --coverage",
63
81
  "test:postgresql": "vitest run test/postgresql.test.ts",
64
82
  "test:mysql": "vitest run test/mysql.test.ts",
83
+ "test:package": "node scripts/release-artifact-smoke.mjs",
84
+ "test:recovery": "pnpm run build && node examples/failure-recovery/demo.ts",
65
85
  "test:redis": "vitest run test/redis-wake-up.test.ts",
66
86
  "test:watch": "vitest",
87
+ "benchmark": "pnpm run build && node benchmarks/run.ts",
88
+ "benchmark:idle": "pnpm run build && node benchmarks/idle.ts",
67
89
  "pack:check": "pnpm pack --dry-run && node scripts/check-package.mjs",
68
90
  "prepack": "pnpm run build"
69
91
  },