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.
- package/CHANGELOG.md +57 -1
- package/README.md +284 -950
- package/dist/actor.d.ts +12 -0
- package/dist/actor.d.ts.map +1 -1
- package/dist/actor.js +25 -2
- package/dist/actor.js.map +1 -1
- package/dist/broadcast-worker.d.ts +2 -0
- package/dist/broadcast-worker.d.ts.map +1 -1
- package/dist/broadcast-worker.js +25 -5
- package/dist/broadcast-worker.js.map +1 -1
- package/dist/browser/components.d.ts.map +1 -1
- package/dist/browser/components.js +4 -1
- package/dist/browser/components.js.map +1 -1
- package/dist/browser/index.d.ts +1 -0
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +8 -0
- package/dist/browser/index.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +13 -1
- package/dist/cli.js.map +1 -1
- package/dist/configuration.d.ts +3 -0
- package/dist/configuration.d.ts.map +1 -1
- package/dist/configuration.js +3 -0
- package/dist/configuration.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +12 -3
- package/dist/doctor.js.map +1 -1
- package/dist/effect-worker.d.ts +2 -0
- package/dist/effect-worker.d.ts.map +1 -1
- package/dist/effect-worker.js +25 -5
- package/dist/effect-worker.js.map +1 -1
- package/dist/examples/sqlite-quickstart.js +83 -0
- package/dist/examples/sqlite-quickstart.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/polling-backoff.d.ts +23 -0
- package/dist/polling-backoff.d.ts.map +1 -0
- package/dist/polling-backoff.js +33 -0
- package/dist/polling-backoff.js.map +1 -0
- package/dist/records.d.ts +1 -0
- package/dist/records.d.ts.map +1 -1
- package/dist/reminder-scheduler.d.ts +2 -0
- package/dist/reminder-scheduler.d.ts.map +1 -1
- package/dist/reminder-scheduler.js +25 -5
- package/dist/reminder-scheduler.js.map +1 -1
- package/dist/repository.d.ts +5 -1
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +15 -4
- package/dist/repository.js.map +1 -1
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +71 -6
- package/dist/runtime.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +15 -4
- package/dist/schema.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wake-up/postgresql.d.ts.map +1 -1
- package/dist/wake-up/postgresql.js +12 -11
- package/dist/wake-up/postgresql.js.map +1 -1
- package/dist/wake-up.d.ts +1 -1
- package/dist/wake-up.d.ts.map +1 -1
- package/dist/wake-up.js +11 -10
- package/dist/wake-up.js.map +1 -1
- package/dist/web/assets.d.ts +4 -0
- package/dist/web/assets.d.ts.map +1 -0
- package/dist/web/assets.js +11 -0
- package/dist/web/assets.js.map +1 -0
- package/dist/web/index.d.ts +28 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +642 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/node.d.ts +3 -0
- package/dist/web/node.d.ts.map +1 -0
- package/dist/web/node.js +109 -0
- package/dist/web/node.js.map +1 -0
- package/dist/web/render.d.ts +64 -0
- package/dist/web/render.d.ts.map +1 -0
- package/dist/web/render.js +331 -0
- package/dist/web/render.js.map +1 -0
- package/dist/web/store.d.ts +65 -0
- package/dist/web/store.d.ts.map +1 -0
- package/dist/web/store.js +303 -0
- package/dist/web/store.js.map +1 -0
- package/dist/web/types.d.ts +82 -0
- package/dist/web/types.d.ts.map +1 -0
- package/dist/web/types.js +2 -0
- package/dist/web/types.js.map +1 -0
- package/dist/worker.d.ts +2 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +25 -5
- package/dist/worker.js.map +1 -1
- package/docs/api.md +70 -4
- package/docs/architecture.md +20 -11
- package/docs/authorization.md +12 -3
- package/docs/benchmarks.md +163 -0
- package/docs/browser-protocol.md +15 -8
- package/docs/comparisons.md +36 -0
- package/docs/configuration.md +14 -3
- package/docs/correctness.md +23 -0
- package/docs/dashboard.md +196 -0
- package/docs/fit.md +58 -0
- package/docs/operations.md +25 -7
- package/docs/parity.md +37 -34
- package/docs/releasing.md +6 -4
- package/docs/state-and-lifecycle.md +10 -0
- package/docs/support.md +39 -0
- package/examples/failure-recovery/actor.ts +51 -0
- package/examples/failure-recovery/demo.ts +233 -0
- package/examples/failure-recovery/worker.ts +46 -0
- package/examples/sqlite-quickstart.ts +109 -0
- 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.
|
|
4
|
-
"description": "
|
|
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
|
},
|