solid-objects 0.13.2 → 0.14.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/CHANGELOG.md +126 -0
- package/README.md +188 -68
- package/dist/actor.d.ts +1 -0
- package/dist/actor.d.ts.map +1 -1
- package/dist/actor.js +9 -0
- package/dist/actor.js.map +1 -1
- package/dist/broadcast-worker.d.ts +1 -1
- package/dist/broadcast-worker.d.ts.map +1 -1
- package/dist/broadcast-worker.js +1 -1
- package/dist/broadcast-worker.js.map +1 -1
- package/dist/broken-pipe.d.ts +5 -0
- package/dist/broken-pipe.d.ts.map +1 -0
- package/dist/broken-pipe.js +8 -0
- package/dist/broken-pipe.js.map +1 -0
- package/dist/browser/host.d.ts +8 -0
- package/dist/browser/host.d.ts.map +1 -0
- package/dist/browser/host.js +22 -0
- package/dist/browser/host.js.map +1 -0
- package/dist/browser/tab-host.d.ts +43 -0
- package/dist/browser/tab-host.d.ts.map +1 -0
- package/dist/browser/tab-host.js +196 -0
- package/dist/browser/tab-host.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -3
- package/dist/cli.js.map +1 -1
- package/dist/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/database/deadline.js +2 -2
- package/dist/database/deadline.js.map +1 -1
- package/dist/database/mysql.d.ts +1 -0
- package/dist/database/mysql.d.ts.map +1 -1
- package/dist/database/mysql.js +1 -0
- package/dist/database/mysql.js.map +1 -1
- package/dist/database/postgresql.d.ts +1 -0
- package/dist/database/postgresql.d.ts.map +1 -1
- package/dist/database/postgresql.js +1 -0
- package/dist/database/postgresql.js.map +1 -1
- package/dist/database/shared-sqlite-wasm.d.ts +74 -0
- package/dist/database/shared-sqlite-wasm.d.ts.map +1 -0
- package/dist/database/shared-sqlite-wasm.js +542 -0
- package/dist/database/shared-sqlite-wasm.js.map +1 -0
- package/dist/database/sqlite-wasm.d.ts +30 -0
- package/dist/database/sqlite-wasm.d.ts.map +1 -0
- package/dist/database/sqlite-wasm.js +185 -0
- package/dist/database/sqlite-wasm.js.map +1 -0
- package/dist/database/sqlite.d.ts +1 -0
- package/dist/database/sqlite.d.ts.map +1 -1
- package/dist/database/sqlite.js +1 -0
- package/dist/database/sqlite.js.map +1 -1
- package/dist/database/transaction-context.js +2 -2
- package/dist/database/transaction-context.js.map +1 -1
- package/dist/doctor.js +1 -1
- package/dist/doctor.js.map +1 -1
- package/dist/effect-worker.d.ts +1 -1
- package/dist/effect-worker.d.ts.map +1 -1
- package/dist/effect-worker.js +1 -1
- package/dist/effect-worker.js.map +1 -1
- package/dist/examples/quickstart-report.js +127 -0
- package/dist/examples/quickstart-report.js.map +1 -0
- package/dist/examples/sqlite-quickstart.js +59 -7
- package/dist/examples/sqlite-quickstart.js.map +1 -1
- package/dist/executable.js +4 -0
- package/dist/executable.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/platform/context-store.d.ts +11 -0
- package/dist/platform/context-store.d.ts.map +1 -0
- package/dist/platform/context-store.js +28 -0
- package/dist/platform/context-store.js.map +1 -0
- package/dist/platform/host-identity.d.ts +11 -0
- package/dist/platform/host-identity.d.ts.map +1 -0
- package/dist/platform/host-identity.js +17 -0
- package/dist/platform/host-identity.js.map +1 -0
- package/dist/platform/node.d.ts +2 -0
- package/dist/platform/node.d.ts.map +1 -0
- package/dist/platform/node.js +11 -0
- package/dist/platform/node.js.map +1 -0
- package/dist/platform/turn-context-store.d.ts +7 -0
- package/dist/platform/turn-context-store.d.ts.map +1 -0
- package/dist/platform/turn-context-store.js +17 -0
- package/dist/platform/turn-context-store.js.map +1 -0
- package/dist/platform/uuid.d.ts +2 -0
- package/dist/platform/uuid.d.ts.map +1 -0
- package/dist/platform/uuid.js +4 -0
- package/dist/platform/uuid.js.map +1 -0
- package/dist/platform/web-locks.d.ts +2 -0
- package/dist/platform/web-locks.d.ts.map +1 -0
- package/dist/platform/web-locks.js +8 -0
- package/dist/platform/web-locks.js.map +1 -0
- package/dist/reminder-scheduler.d.ts +1 -1
- package/dist/reminder-scheduler.d.ts.map +1 -1
- package/dist/reminder-scheduler.js +1 -1
- package/dist/reminder-scheduler.js.map +1 -1
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +13 -6
- package/dist/repository.js.map +1 -1
- package/dist/runtime.d.ts +25 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +62 -1
- package/dist/runtime.js.map +1 -1
- package/dist/serialization.d.ts.map +1 -1
- package/dist/serialization.js +2 -1
- package/dist/serialization.js.map +1 -1
- package/dist/transmit-effect.d.ts +2 -0
- package/dist/transmit-effect.d.ts.map +1 -0
- package/dist/transmit-effect.js +2 -0
- package/dist/transmit-effect.js.map +1 -0
- package/dist/transmit.d.ts +27 -0
- package/dist/transmit.d.ts.map +1 -0
- package/dist/transmit.js +105 -0
- package/dist/transmit.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/web/index.d.ts +1 -0
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +1 -0
- package/dist/web/index.js.map +1 -1
- package/dist/worker.d.ts +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/docs/api.md +199 -20
- package/docs/architecture.md +50 -23
- package/docs/authorization.md +6 -5
- package/docs/benchmarks.md +87 -65
- package/docs/browser-protocol.md +62 -3
- package/docs/comparisons.md +14 -9
- package/docs/configuration.md +2 -2
- package/docs/correctness.md +30 -17
- package/docs/dashboard.md +6 -6
- package/docs/errors-and-recovery.md +12 -12
- package/docs/fit.md +3 -0
- package/docs/operations.md +33 -19
- package/docs/parity.md +126 -47
- package/docs/releasing.md +11 -5
- package/docs/state-and-lifecycle.md +12 -8
- package/docs/support.md +56 -20
- package/examples/failure-recovery/actor.ts +6 -8
- package/examples/failure-recovery/demo.ts +8 -26
- package/examples/failure-recovery/serialization.ts +133 -0
- package/examples/quickstart-report.ts +151 -0
- package/examples/sqlite-quickstart.ts +76 -16
- package/package.json +29 -3
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
|
|
3
|
+
export interface SerializationEvent {
|
|
4
|
+
event: "start" | "finish"
|
|
5
|
+
messageId: string
|
|
6
|
+
attempt: number
|
|
7
|
+
processId: number
|
|
8
|
+
at: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SerializationProof {
|
|
12
|
+
executions: number
|
|
13
|
+
retried: boolean
|
|
14
|
+
supersededOverlap: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Execution {
|
|
18
|
+
messageId: string
|
|
19
|
+
attempt: number
|
|
20
|
+
processId: number
|
|
21
|
+
startedAt: number
|
|
22
|
+
finishedAt: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseSerializationEvent(line: string): SerializationEvent {
|
|
26
|
+
const event = JSON.parse(line) as Partial<SerializationEvent>
|
|
27
|
+
if (
|
|
28
|
+
(event.event !== "start" && event.event !== "finish") ||
|
|
29
|
+
typeof event.messageId !== "string" ||
|
|
30
|
+
typeof event.attempt !== "number" ||
|
|
31
|
+
typeof event.processId !== "number" ||
|
|
32
|
+
typeof event.at !== "number"
|
|
33
|
+
) {
|
|
34
|
+
throw new TypeError("invalid serialization event")
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
event: event.event,
|
|
38
|
+
messageId: event.messageId,
|
|
39
|
+
attempt: event.attempt,
|
|
40
|
+
processId: event.processId,
|
|
41
|
+
at: event.at,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// One identity commits one state transition at a time. The control file is
|
|
46
|
+
// written outside the transaction, so it records execution attempts rather than
|
|
47
|
+
// commits: a worker that loses its lease keeps running until it notices, and its
|
|
48
|
+
// replacement executes the same message under a higher attempt. The superseded
|
|
49
|
+
// attempt may therefore overlap anything, because its write is fenced out and
|
|
50
|
+
// the committed state is what proves it.
|
|
51
|
+
//
|
|
52
|
+
// Each event carries its attempt and process, so a start pairs with its own
|
|
53
|
+
// finish rather than with whichever finish arrived next. Without that, a
|
|
54
|
+
// superseded attempt finishing late reads as its replacement finishing, and a
|
|
55
|
+
// second message could then overlap a replacement that is still running.
|
|
56
|
+
export function assertSerializedExecution(
|
|
57
|
+
events: readonly SerializationEvent[],
|
|
58
|
+
options: { messageCount: number },
|
|
59
|
+
): SerializationProof {
|
|
60
|
+
const executions = pairExecutions(events)
|
|
61
|
+
|
|
62
|
+
const messageIds = new Set(executions.map((execution) => execution.messageId))
|
|
63
|
+
assert.equal(
|
|
64
|
+
messageIds.size,
|
|
65
|
+
options.messageCount,
|
|
66
|
+
`expected ${options.messageCount} messages to run, saw ${messageIds.size}`,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const survivingAttempt = new Map<string, number>()
|
|
70
|
+
for (const execution of executions) {
|
|
71
|
+
const highest = survivingAttempt.get(execution.messageId) ?? 0
|
|
72
|
+
if (execution.attempt > highest) survivingAttempt.set(execution.messageId, execution.attempt)
|
|
73
|
+
}
|
|
74
|
+
const surviving = executions.filter(
|
|
75
|
+
(execution) => survivingAttempt.get(execution.messageId) === execution.attempt,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
for (const [index, execution] of surviving.entries()) {
|
|
79
|
+
for (const other of surviving.slice(index + 1)) {
|
|
80
|
+
assert(
|
|
81
|
+
!overlaps(execution, other),
|
|
82
|
+
`${describe(execution)} and ${describe(other)} overlap, and neither was superseded`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const supersededOverlap = executions.some((execution) =>
|
|
88
|
+
executions.some((other) => other !== execution && overlaps(execution, other)),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
executions: executions.length,
|
|
93
|
+
retried: executions.length > options.messageCount,
|
|
94
|
+
supersededOverlap,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function pairExecutions(events: readonly SerializationEvent[]): Execution[] {
|
|
99
|
+
const started = new Map<string, SerializationEvent>()
|
|
100
|
+
const executions: Execution[] = []
|
|
101
|
+
|
|
102
|
+
for (const event of [...events].sort((left, right) => left.at - right.at)) {
|
|
103
|
+
const key = `${event.messageId}#${event.attempt}#${event.processId}`
|
|
104
|
+
if (event.event === "start") {
|
|
105
|
+
assert(!started.has(key), `${describe(event)} started twice`)
|
|
106
|
+
started.set(key, event)
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
const start = started.get(key)
|
|
110
|
+
assert(start !== undefined, `${describe(event)} finished with no matching start`)
|
|
111
|
+
started.delete(key)
|
|
112
|
+
executions.push({
|
|
113
|
+
messageId: event.messageId,
|
|
114
|
+
attempt: event.attempt,
|
|
115
|
+
processId: event.processId,
|
|
116
|
+
startedAt: start.at,
|
|
117
|
+
finishedAt: event.at,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const unfinished = [...started.values()].map(describe)
|
|
122
|
+
assert.equal(unfinished.length, 0, `${unfinished.join(", ")} never wrote a finish`)
|
|
123
|
+
|
|
124
|
+
return executions
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function overlaps(left: Execution, right: Execution): boolean {
|
|
128
|
+
return left.startedAt < right.finishedAt && right.startedAt < left.finishedAt
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function describe(execution: { messageId: string; attempt: number }): string {
|
|
132
|
+
return `${execution.messageId} attempt ${execution.attempt}`
|
|
133
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
export interface QuickstartSummary {
|
|
2
|
+
sameIdentityCalls: number
|
|
3
|
+
sameIdentityFinalState: number
|
|
4
|
+
independentIdentitiesOverlapped: boolean
|
|
5
|
+
temporaryStateRemoved: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface QuickstartCheck {
|
|
9
|
+
passed: boolean
|
|
10
|
+
title: string
|
|
11
|
+
detail: readonly string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const INSTALL_COMMAND = "npm install solid-objects"
|
|
15
|
+
const AGENT_PROMPT = "where would the solid-objects library be best used in this app?"
|
|
16
|
+
const DOCUMENTATION_URL = "https://solidobjects.dev/node"
|
|
17
|
+
|
|
18
|
+
export function formatQuickstartPlan(plan: {
|
|
19
|
+
sameIdentityCalls: number
|
|
20
|
+
actorSource: string
|
|
21
|
+
}): string {
|
|
22
|
+
return [
|
|
23
|
+
"Solid Objects quickstart",
|
|
24
|
+
"",
|
|
25
|
+
"This command will:",
|
|
26
|
+
"",
|
|
27
|
+
" 1. create a temporary SQLite database;",
|
|
28
|
+
` 2. send ${plan.sameIdentityCalls} concurrent calls to one identity;`,
|
|
29
|
+
" 3. run two other identities at the same time;",
|
|
30
|
+
" 4. close the runtime and delete the temporary database.",
|
|
31
|
+
"",
|
|
32
|
+
"No server, container, or configuration is needed.",
|
|
33
|
+
"",
|
|
34
|
+
"The actor it runs:",
|
|
35
|
+
"",
|
|
36
|
+
...plan.actorSource.split("\n").map((line) => ` ${line}`.trimEnd()),
|
|
37
|
+
"",
|
|
38
|
+
"",
|
|
39
|
+
].join("\n")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatQuickstartPrompt(): string {
|
|
43
|
+
return "Run it now? [Y/n] "
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function answerAllowsRun(answer: string): boolean {
|
|
47
|
+
const value = answer.trim().toLowerCase()
|
|
48
|
+
if (value === "") return true
|
|
49
|
+
return value === "y" || value === "yes"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatQuickstartStop(): string {
|
|
53
|
+
return ["", "Nothing ran. No database and no files were created.", ""].join("\n")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatQuickstartReport(summary: QuickstartSummary): string {
|
|
57
|
+
const results = checks(summary)
|
|
58
|
+
return [
|
|
59
|
+
"Results",
|
|
60
|
+
"",
|
|
61
|
+
...results.flatMap(checkLines),
|
|
62
|
+
"",
|
|
63
|
+
"Each line above is an assertion, not a print. The command exits with a",
|
|
64
|
+
"non-zero code when one of them fails.",
|
|
65
|
+
"",
|
|
66
|
+
...meaning(results),
|
|
67
|
+
"Add it to your app",
|
|
68
|
+
"",
|
|
69
|
+
` ${INSTALL_COMMAND}`,
|
|
70
|
+
"",
|
|
71
|
+
"Or tell your agent",
|
|
72
|
+
"",
|
|
73
|
+
` ${AGENT_PROMPT}`,
|
|
74
|
+
"",
|
|
75
|
+
`Docs: ${DOCUMENTATION_URL}`,
|
|
76
|
+
"",
|
|
77
|
+
].join("\n")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function meaning(results: readonly QuickstartCheck[]): string[] {
|
|
81
|
+
if (results.some((check) => !check.passed)) return []
|
|
82
|
+
return [
|
|
83
|
+
"What each PASS means",
|
|
84
|
+
"",
|
|
85
|
+
" Two requests on the same cart cannot overwrite each other.",
|
|
86
|
+
" That identity has one durable mailbox, so its calls commit one at a",
|
|
87
|
+
" time and no update is lost.",
|
|
88
|
+
"",
|
|
89
|
+
" Unrelated identities do not wait for that mailbox.",
|
|
90
|
+
" The order is per identity, not global.",
|
|
91
|
+
"",
|
|
92
|
+
" The state lives in an ordinary SQL database, so it survives a restart.",
|
|
93
|
+
"",
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function checkLines(check: QuickstartCheck): string[] {
|
|
98
|
+
const result = check.passed ? "PASS" : "FAIL"
|
|
99
|
+
return [`${result} ${check.title}`, ...check.detail.map((line) => ` ${line}`)]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function checks(summary: QuickstartSummary): QuickstartCheck[] {
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
passed: summary.sameIdentityFinalState === summary.sameIdentityCalls,
|
|
106
|
+
title: `${summary.sameIdentityCalls} concurrent calls to one identity`,
|
|
107
|
+
detail: sameIdentityDetail(summary),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
passed: summary.independentIdentitiesOverlapped,
|
|
111
|
+
title: "Two different identities ran at the same time",
|
|
112
|
+
detail: overlapDetail(summary),
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
passed: summary.temporaryStateRemoved,
|
|
116
|
+
title: "Temporary state removed",
|
|
117
|
+
detail: cleanupDetail(summary),
|
|
118
|
+
},
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function sameIdentityDetail(summary: QuickstartSummary): string[] {
|
|
123
|
+
if (summary.sameIdentityFinalState !== summary.sameIdentityCalls) {
|
|
124
|
+
return [
|
|
125
|
+
`The committed state is ${summary.sameIdentityFinalState} after ${summary.sameIdentityCalls} calls.`,
|
|
126
|
+
"The runtime lost an update.",
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
return [
|
|
130
|
+
"They ran in order on one mailbox.",
|
|
131
|
+
`The committed state is ${summary.sameIdentityFinalState}.`,
|
|
132
|
+
`The return values were the complete sequence 1 through ${summary.sameIdentityCalls}.`,
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function overlapDetail(summary: QuickstartSummary): string[] {
|
|
137
|
+
if (!summary.independentIdentitiesOverlapped) {
|
|
138
|
+
return ["Their execution windows did not overlap."]
|
|
139
|
+
}
|
|
140
|
+
return [
|
|
141
|
+
"Their execution windows overlapped, so an unrelated identity",
|
|
142
|
+
"never waits behind this one.",
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function cleanupDetail(summary: QuickstartSummary): string[] {
|
|
147
|
+
if (!summary.temporaryStateRemoved) {
|
|
148
|
+
return ["The temporary SQLite database is still on disk."]
|
|
149
|
+
}
|
|
150
|
+
return ["The scoped temporary SQLite database was deleted at exit."]
|
|
151
|
+
}
|
|
@@ -6,6 +6,17 @@ import { join, resolve } from "node:path"
|
|
|
6
6
|
import { fileURLToPath } from "node:url"
|
|
7
7
|
import { Actor, createRuntime } from "solid-objects"
|
|
8
8
|
import { sqlite } from "solid-objects/database/sqlite"
|
|
9
|
+
import { createInterface } from "node:readline/promises"
|
|
10
|
+
import {
|
|
11
|
+
answerAllowsRun,
|
|
12
|
+
formatQuickstartPlan,
|
|
13
|
+
formatQuickstartPrompt,
|
|
14
|
+
formatQuickstartReport,
|
|
15
|
+
formatQuickstartStop,
|
|
16
|
+
type QuickstartSummary,
|
|
17
|
+
} from "./quickstart-report.js"
|
|
18
|
+
|
|
19
|
+
const SAME_IDENTITY_CALLS = 25
|
|
9
20
|
|
|
10
21
|
class Counter extends Actor {
|
|
11
22
|
static override readonly actorType = "QuickstartCounter"
|
|
@@ -27,12 +38,61 @@ class Counter extends Actor {
|
|
|
27
38
|
}
|
|
28
39
|
}
|
|
29
40
|
|
|
41
|
+
const COUNTER_SOURCE = `class Counter extends Actor {
|
|
42
|
+
static override readonly actorType = "QuickstartCounter"
|
|
43
|
+
|
|
44
|
+
count = 0
|
|
45
|
+
|
|
46
|
+
increment(): number {
|
|
47
|
+
this.count += 1
|
|
48
|
+
return this.count
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async pause({ milliseconds }: { milliseconds: number }) {
|
|
52
|
+
const startedAt = performance.now()
|
|
53
|
+
await new Promise((done) => setTimeout(done, milliseconds))
|
|
54
|
+
return { startedAt, finishedAt: performance.now() }
|
|
55
|
+
}
|
|
56
|
+
}`
|
|
57
|
+
|
|
58
|
+
async function confirmRun(options: {
|
|
59
|
+
format?: "report" | "json"
|
|
60
|
+
confirm?: () => boolean | Promise<boolean>
|
|
61
|
+
}): Promise<boolean> {
|
|
62
|
+
if (options.format === "json") return true
|
|
63
|
+
if (options.confirm) return options.confirm()
|
|
64
|
+
if (!process.stdin.isTTY) return true
|
|
65
|
+
const questions = createInterface({ input: process.stdin, output: process.stdout })
|
|
66
|
+
try {
|
|
67
|
+
return answerAllowsRun(await questions.question(formatQuickstartPrompt()))
|
|
68
|
+
} catch {
|
|
69
|
+
return false
|
|
70
|
+
} finally {
|
|
71
|
+
questions.close()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
30
75
|
export async function runQuickstart(
|
|
31
76
|
options: {
|
|
32
77
|
signal?: AbortSignal
|
|
33
78
|
write?: (value: string) => void
|
|
79
|
+
format?: "report" | "json"
|
|
80
|
+
confirm?: () => boolean | Promise<boolean>
|
|
34
81
|
} = {},
|
|
35
82
|
): Promise<void> {
|
|
83
|
+
const write = options.write ?? ((value: string) => process.stdout.write(value))
|
|
84
|
+
if (options.format !== "json") {
|
|
85
|
+
write(
|
|
86
|
+
formatQuickstartPlan({
|
|
87
|
+
sameIdentityCalls: SAME_IDENTITY_CALLS,
|
|
88
|
+
actorSource: COUNTER_SOURCE,
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
if (!(await confirmRun(options))) {
|
|
93
|
+
write(formatQuickstartStop())
|
|
94
|
+
return
|
|
95
|
+
}
|
|
36
96
|
const directory = await mkdtemp(join(tmpdir(), "solid-objects-quickstart-"))
|
|
37
97
|
const databasePath = join(directory, "state.sqlite3")
|
|
38
98
|
const runtime = createRuntime({
|
|
@@ -58,13 +118,15 @@ export async function runQuickstart(
|
|
|
58
118
|
running = runtime.run(shutdown.signal)
|
|
59
119
|
|
|
60
120
|
const counter = runtime.ref(Counter, "room-1")
|
|
61
|
-
const results = await Promise.all(
|
|
121
|
+
const results = await Promise.all(
|
|
122
|
+
Array.from({ length: SAME_IDENTITY_CALLS }, () => counter.increment()),
|
|
123
|
+
)
|
|
62
124
|
assert.deepEqual(
|
|
63
125
|
[...results].sort((left, right) => left - right),
|
|
64
|
-
Array.from({ length:
|
|
126
|
+
Array.from({ length: SAME_IDENTITY_CALLS }, (_value, index) => index + 1),
|
|
65
127
|
)
|
|
66
128
|
sameIdentityFinalState = await counter.count
|
|
67
|
-
assert.equal(sameIdentityFinalState,
|
|
129
|
+
assert.equal(sameIdentityFinalState, SAME_IDENTITY_CALLS)
|
|
68
130
|
|
|
69
131
|
const pauses = await Promise.all([
|
|
70
132
|
runtime.ref(Counter, "room-2").send.pause({ milliseconds: 100 }),
|
|
@@ -89,19 +151,17 @@ export async function runQuickstart(
|
|
|
89
151
|
}
|
|
90
152
|
|
|
91
153
|
assert.equal(existsSync(directory), false)
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
)}\n`,
|
|
104
|
-
)
|
|
154
|
+
const summary: QuickstartSummary = {
|
|
155
|
+
sameIdentityCalls: SAME_IDENTITY_CALLS,
|
|
156
|
+
sameIdentityFinalState,
|
|
157
|
+
independentIdentitiesOverlapped,
|
|
158
|
+
temporaryStateRemoved: true,
|
|
159
|
+
}
|
|
160
|
+
if (options.format === "json") {
|
|
161
|
+
write(`${JSON.stringify(summary, null, 2)}\n`)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
write(formatQuickstartReport(summary))
|
|
105
165
|
}
|
|
106
166
|
|
|
107
167
|
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid-objects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Race-free realtime state per application identity, backed by your SQL database",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"url": "git+https://github.com/cardmagic/solid-objects-js.git"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
|
-
"node": ">=24.
|
|
26
|
+
"node": ">=24.4.0"
|
|
27
27
|
},
|
|
28
28
|
"packageManager": "pnpm@11.11.0",
|
|
29
29
|
"bin": {
|
|
@@ -46,6 +46,14 @@
|
|
|
46
46
|
"types": "./dist/database/sqlite.d.ts",
|
|
47
47
|
"import": "./dist/database/sqlite.js"
|
|
48
48
|
},
|
|
49
|
+
"./database/sqlite-wasm": {
|
|
50
|
+
"types": "./dist/database/sqlite-wasm.d.ts",
|
|
51
|
+
"import": "./dist/database/sqlite-wasm.js"
|
|
52
|
+
},
|
|
53
|
+
"./database/shared-sqlite-wasm": {
|
|
54
|
+
"types": "./dist/database/shared-sqlite-wasm.d.ts",
|
|
55
|
+
"import": "./dist/database/shared-sqlite-wasm.js"
|
|
56
|
+
},
|
|
49
57
|
"./database/postgresql": {
|
|
50
58
|
"types": "./dist/database/postgresql.d.ts",
|
|
51
59
|
"import": "./dist/database/postgresql.js"
|
|
@@ -62,6 +70,18 @@
|
|
|
62
70
|
"types": "./dist/browser/index.d.ts",
|
|
63
71
|
"import": "./dist/browser/index.js"
|
|
64
72
|
},
|
|
73
|
+
"./browser/host": {
|
|
74
|
+
"types": "./dist/browser/host.d.ts",
|
|
75
|
+
"import": "./dist/browser/host.js"
|
|
76
|
+
},
|
|
77
|
+
"./browser/tab-host": {
|
|
78
|
+
"types": "./dist/browser/tab-host.d.ts",
|
|
79
|
+
"import": "./dist/browser/tab-host.js"
|
|
80
|
+
},
|
|
81
|
+
"./transmit": {
|
|
82
|
+
"types": "./dist/transmit.d.ts",
|
|
83
|
+
"import": "./dist/transmit.js"
|
|
84
|
+
},
|
|
65
85
|
"./web": {
|
|
66
86
|
"types": "./dist/web/index.d.ts",
|
|
67
87
|
"import": "./dist/web/index.js"
|
|
@@ -69,7 +89,8 @@
|
|
|
69
89
|
},
|
|
70
90
|
"scripts": {
|
|
71
91
|
"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",
|
|
92
|
+
"check": "pnpm run check:parameters && pnpm run check:documentation && pnpm run check:browser-imports && tsc -p tsconfig.json --noEmit && tsc -p tsconfig.examples.json --noEmit",
|
|
93
|
+
"check:browser-imports": "node scripts/check-browser-imports.mjs",
|
|
73
94
|
"check:documentation": "node scripts/check-documentation.mjs",
|
|
74
95
|
"check:parameters": "node scripts/check-parameter-style.mjs",
|
|
75
96
|
"clean": "node scripts/clean.mjs",
|
|
@@ -91,6 +112,7 @@
|
|
|
91
112
|
},
|
|
92
113
|
"devDependencies": {
|
|
93
114
|
"@playwright/test": "^1.62.1",
|
|
115
|
+
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
94
116
|
"@types/node": "^24.0.0",
|
|
95
117
|
"@types/pg": "^8.21.0",
|
|
96
118
|
"@types/ws": "^8.18.1",
|
|
@@ -104,11 +126,15 @@
|
|
|
104
126
|
"ws": "^8.21.3"
|
|
105
127
|
},
|
|
106
128
|
"peerDependencies": {
|
|
129
|
+
"@sqlite.org/sqlite-wasm": ">=3.50.0-build1",
|
|
107
130
|
"mysql2": "^3.23.3",
|
|
108
131
|
"pg": "^8.23.0",
|
|
109
132
|
"redis": "^6.2.1"
|
|
110
133
|
},
|
|
111
134
|
"peerDependenciesMeta": {
|
|
135
|
+
"@sqlite.org/sqlite-wasm": {
|
|
136
|
+
"optional": true
|
|
137
|
+
},
|
|
112
138
|
"mysql2": {
|
|
113
139
|
"optional": true
|
|
114
140
|
},
|