solid-objects 0.13.3 → 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.
Files changed (143) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +185 -67
  3. package/dist/actor.d.ts +1 -0
  4. package/dist/actor.d.ts.map +1 -1
  5. package/dist/actor.js +9 -0
  6. package/dist/actor.js.map +1 -1
  7. package/dist/broadcast-worker.d.ts +1 -1
  8. package/dist/broadcast-worker.d.ts.map +1 -1
  9. package/dist/broadcast-worker.js +1 -1
  10. package/dist/broadcast-worker.js.map +1 -1
  11. package/dist/broken-pipe.d.ts +5 -0
  12. package/dist/broken-pipe.d.ts.map +1 -0
  13. package/dist/broken-pipe.js +8 -0
  14. package/dist/broken-pipe.js.map +1 -0
  15. package/dist/browser/host.d.ts +8 -0
  16. package/dist/browser/host.d.ts.map +1 -0
  17. package/dist/browser/host.js +22 -0
  18. package/dist/browser/host.js.map +1 -0
  19. package/dist/browser/tab-host.d.ts +43 -0
  20. package/dist/browser/tab-host.d.ts.map +1 -0
  21. package/dist/browser/tab-host.js +196 -0
  22. package/dist/browser/tab-host.js.map +1 -0
  23. package/dist/cli.d.ts +1 -0
  24. package/dist/cli.d.ts.map +1 -1
  25. package/dist/cli.js +6 -3
  26. package/dist/cli.js.map +1 -1
  27. package/dist/context.js +2 -2
  28. package/dist/context.js.map +1 -1
  29. package/dist/database/deadline.js +2 -2
  30. package/dist/database/deadline.js.map +1 -1
  31. package/dist/database/mysql.d.ts +1 -0
  32. package/dist/database/mysql.d.ts.map +1 -1
  33. package/dist/database/mysql.js +1 -0
  34. package/dist/database/mysql.js.map +1 -1
  35. package/dist/database/postgresql.d.ts +1 -0
  36. package/dist/database/postgresql.d.ts.map +1 -1
  37. package/dist/database/postgresql.js +1 -0
  38. package/dist/database/postgresql.js.map +1 -1
  39. package/dist/database/shared-sqlite-wasm.d.ts +74 -0
  40. package/dist/database/shared-sqlite-wasm.d.ts.map +1 -0
  41. package/dist/database/shared-sqlite-wasm.js +542 -0
  42. package/dist/database/shared-sqlite-wasm.js.map +1 -0
  43. package/dist/database/sqlite-wasm.d.ts +30 -0
  44. package/dist/database/sqlite-wasm.d.ts.map +1 -0
  45. package/dist/database/sqlite-wasm.js +185 -0
  46. package/dist/database/sqlite-wasm.js.map +1 -0
  47. package/dist/database/sqlite.d.ts +1 -0
  48. package/dist/database/sqlite.d.ts.map +1 -1
  49. package/dist/database/sqlite.js +1 -0
  50. package/dist/database/sqlite.js.map +1 -1
  51. package/dist/database/transaction-context.js +2 -2
  52. package/dist/database/transaction-context.js.map +1 -1
  53. package/dist/doctor.js +1 -1
  54. package/dist/doctor.js.map +1 -1
  55. package/dist/effect-worker.d.ts +1 -1
  56. package/dist/effect-worker.d.ts.map +1 -1
  57. package/dist/effect-worker.js +1 -1
  58. package/dist/effect-worker.js.map +1 -1
  59. package/dist/examples/quickstart-report.js +127 -0
  60. package/dist/examples/quickstart-report.js.map +1 -0
  61. package/dist/examples/sqlite-quickstart.js +59 -7
  62. package/dist/examples/sqlite-quickstart.js.map +1 -1
  63. package/dist/executable.js +4 -0
  64. package/dist/executable.js.map +1 -1
  65. package/dist/index.d.ts +3 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +3 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/platform/context-store.d.ts +11 -0
  70. package/dist/platform/context-store.d.ts.map +1 -0
  71. package/dist/platform/context-store.js +28 -0
  72. package/dist/platform/context-store.js.map +1 -0
  73. package/dist/platform/host-identity.d.ts +11 -0
  74. package/dist/platform/host-identity.d.ts.map +1 -0
  75. package/dist/platform/host-identity.js +17 -0
  76. package/dist/platform/host-identity.js.map +1 -0
  77. package/dist/platform/node.d.ts +2 -0
  78. package/dist/platform/node.d.ts.map +1 -0
  79. package/dist/platform/node.js +11 -0
  80. package/dist/platform/node.js.map +1 -0
  81. package/dist/platform/turn-context-store.d.ts +7 -0
  82. package/dist/platform/turn-context-store.d.ts.map +1 -0
  83. package/dist/platform/turn-context-store.js +17 -0
  84. package/dist/platform/turn-context-store.js.map +1 -0
  85. package/dist/platform/uuid.d.ts +2 -0
  86. package/dist/platform/uuid.d.ts.map +1 -0
  87. package/dist/platform/uuid.js +4 -0
  88. package/dist/platform/uuid.js.map +1 -0
  89. package/dist/platform/web-locks.d.ts +2 -0
  90. package/dist/platform/web-locks.d.ts.map +1 -0
  91. package/dist/platform/web-locks.js +8 -0
  92. package/dist/platform/web-locks.js.map +1 -0
  93. package/dist/reminder-scheduler.d.ts +1 -1
  94. package/dist/reminder-scheduler.d.ts.map +1 -1
  95. package/dist/reminder-scheduler.js +1 -1
  96. package/dist/reminder-scheduler.js.map +1 -1
  97. package/dist/repository.d.ts.map +1 -1
  98. package/dist/repository.js +13 -6
  99. package/dist/repository.js.map +1 -1
  100. package/dist/runtime.d.ts +25 -1
  101. package/dist/runtime.d.ts.map +1 -1
  102. package/dist/runtime.js +62 -1
  103. package/dist/runtime.js.map +1 -1
  104. package/dist/serialization.d.ts.map +1 -1
  105. package/dist/serialization.js +2 -1
  106. package/dist/serialization.js.map +1 -1
  107. package/dist/transmit-effect.d.ts +2 -0
  108. package/dist/transmit-effect.d.ts.map +1 -0
  109. package/dist/transmit-effect.js +2 -0
  110. package/dist/transmit-effect.js.map +1 -0
  111. package/dist/transmit.d.ts +27 -0
  112. package/dist/transmit.d.ts.map +1 -0
  113. package/dist/transmit.js +105 -0
  114. package/dist/transmit.js.map +1 -0
  115. package/dist/version.d.ts +1 -1
  116. package/dist/version.js +1 -1
  117. package/dist/web/index.d.ts +1 -0
  118. package/dist/web/index.d.ts.map +1 -1
  119. package/dist/web/index.js +1 -0
  120. package/dist/web/index.js.map +1 -1
  121. package/dist/worker.d.ts +1 -1
  122. package/dist/worker.d.ts.map +1 -1
  123. package/dist/worker.js +1 -1
  124. package/dist/worker.js.map +1 -1
  125. package/docs/api.md +199 -20
  126. package/docs/architecture.md +50 -23
  127. package/docs/authorization.md +6 -5
  128. package/docs/benchmarks.md +87 -65
  129. package/docs/browser-protocol.md +62 -3
  130. package/docs/comparisons.md +14 -9
  131. package/docs/configuration.md +2 -2
  132. package/docs/correctness.md +30 -17
  133. package/docs/dashboard.md +6 -6
  134. package/docs/errors-and-recovery.md +12 -12
  135. package/docs/fit.md +3 -0
  136. package/docs/operations.md +33 -19
  137. package/docs/parity.md +125 -46
  138. package/docs/releasing.md +6 -6
  139. package/docs/state-and-lifecycle.md +12 -8
  140. package/docs/support.md +45 -20
  141. package/examples/quickstart-report.ts +151 -0
  142. package/examples/sqlite-quickstart.ts +76 -16
  143. package/package.json +28 -2
@@ -83,72 +83,93 @@ dataset. Redirect stdout to retain the JSON result.
83
83
 
84
84
  ## Observed results
85
85
 
86
- Measured on August 15, 2026 with the prepared `0.13.0` source tree:
87
-
88
- - Apple M5, 10 logical CPUs, 24 GiB memory
89
- - macOS 26.6 (`darwin 25.6.0`)
90
- - Node.js 26.7.0
91
- - SQLite 3.53.4 on the internal SSD, PostgreSQL 18.4 and MySQL 8.4.11 in
92
- Docker Desktop
86
+ Measured on August 22, 2026 with the `0.14.0` source tree:
87
+
88
+ - Apple M5 (Mac17,2), 10 logical CPUs
89
+ - macOS 26.6
90
+ - Node.js 24.18.0
91
+ - SQLite 3.53.1 through `node:sqlite` on the internal SSD
92
+ - PostgreSQL 17.11 and MySQL 9.7.1, both installed natively and started on a
93
+ scoped temporary data directory
93
94
  - 25 warmup operations, 250 measured operations, concurrency 16
94
95
 
95
- ### SQLite 3.53.4
96
-
97
- | Topology | Shape | Handler | ops/s | p50 ms | p95 ms | p99 ms |
98
- | -------------- | --------- | ------------ | -----: | -----: | ------: | ------: |
99
- | one process | warm hot | synchronous | 95.51 | 35.34 | 1159.07 | 1711.76 |
100
- | one process | warm hot | asynchronous | 448.63 | 35.96 | 38.95 | 41.47 |
101
- | one process | warm many | synchronous | 44.35 | 141.55 | 1886.35 | 2543.15 |
102
- | one process | warm many | asynchronous | 240.64 | 48.62 | 75.10 | 671.64 |
103
- | one process | cold many | synchronous | 30.91 | 436.98 | 1502.26 | 1729.10 |
104
- | one process | cold many | asynchronous | 57.15 | 167.36 | 967.74 | 1066.49 |
105
- | four processes | warm hot | synchronous | 453.10 | 27.24 | 72.09 | 77.88 |
106
- | four processes | warm hot | asynchronous | 487.39 | 29.27 | 60.66 | 68.28 |
107
- | four processes | warm many | synchronous | 78.88 | 86.33 | 890.29 | 1381.65 |
108
- | four processes | warm many | asynchronous | 47.56 | 220.37 | 1130.53 | 1341.86 |
109
- | four processes | cold many | synchronous | 70.45 | 174.67 | 643.53 | 668.46 |
110
- | four processes | cold many | asynchronous | 39.92 | 221.01 | 1309.13 | 1442.30 |
111
-
112
- ### PostgreSQL 18.4
113
-
114
- | Topology | Shape | Handler | ops/s | p50 ms | p95 ms | p99 ms |
115
- | -------------- | --------- | ------------ | ----: | ------: | ------: | ------: |
116
- | one process | warm hot | synchronous | 66.99 | 232.89 | 317.94 | 375.68 |
117
- | one process | warm hot | asynchronous | 72.27 | 211.57 | 280.89 | 316.61 |
118
- | one process | warm many | synchronous | 76.69 | 129.88 | 552.01 | 1963.27 |
119
- | one process | warm many | asynchronous | 25.78 | 340.96 | 2326.38 | 6917.26 |
120
- | one process | cold many | synchronous | 12.70 | 1262.08 | 1734.77 | 2199.20 |
121
- | one process | cold many | asynchronous | 11.23 | 1254.46 | 2796.81 | 2905.99 |
122
- | four processes | warm hot | synchronous | 83.71 | 191.83 | 220.87 | 231.84 |
123
- | four processes | warm hot | asynchronous | 86.42 | 184.56 | 210.11 | 215.48 |
124
- | four processes | warm many | synchronous | 95.47 | 100.31 | 330.27 | 1663.84 |
125
- | four processes | warm many | asynchronous | 37.04 | 232.71 | 1601.29 | 4447.85 |
126
- | four processes | cold many | synchronous | 14.79 | 1114.18 | 1263.33 | 1313.70 |
127
- | four processes | cold many | asynchronous | 11.68 | 1231.94 | 2555.34 | 2848.80 |
128
-
129
- ### MySQL 8.4.11
130
-
131
- | Topology | Shape | Handler | ops/s | p50 ms | p95 ms | p99 ms |
132
- | -------------- | --------- | ------------ | ----: | ------: | ------: | ------: |
133
- | one process | warm hot | synchronous | 28.11 | 504.36 | 1648.10 | 2088.05 |
134
- | one process | warm hot | asynchronous | 25.29 | 508.26 | 1750.17 | 2222.72 |
135
- | one process | warm many | synchronous | 61.79 | 165.62 | 463.59 | 2036.31 |
136
- | one process | warm many | asynchronous | 22.45 | 446.17 | 2420.92 | 7818.65 |
137
- | one process | cold many | synchronous | 10.23 | 1353.37 | 3038.76 | 3185.15 |
138
- | one process | cold many | asynchronous | 10.04 | 1338.31 | 3344.18 | 3477.06 |
139
- | four processes | warm hot | synchronous | 29.89 | 427.18 | 1483.88 | 1997.56 |
140
- | four processes | warm hot | asynchronous | 27.59 | 448.16 | 1518.00 | 1589.57 |
141
- | four processes | warm many | synchronous | 69.77 | 157.79 | 410.83 | 2263.13 |
142
- | four processes | warm many | asynchronous | 36.21 | 265.24 | 1579.43 | 4594.33 |
143
- | four processes | cold many | synchronous | 13.68 | 1088.93 | 2186.30 | 2519.92 |
144
- | four processes | cold many | asynchronous | 11.38 | 1209.84 | 2532.38 | 2730.74 |
145
-
146
- The poor throughput and tail latency in cold and asynchronous cases are
147
- observed limitations, not capacity recommendations. The small asynchronous
148
- yield changed scheduling enough to improve some cases and worsen others;
149
- repeat runs on application-shaped payloads are required before drawing a
150
- general conclusion. PostgreSQL 14, MySQL 8.0, and other database versions are
151
- covered by integration tests but were not benchmarked.
96
+ ### SQLite 3.53.1
97
+
98
+ | Topology | Shape | Handler | ops/s | p50 ms | p95 ms | p99 ms |
99
+ | -------------- | --------- | ------------ | -----: | -----: | -----: | -----: |
100
+ | one process | warm hot | synchronous | 286.09 | 50.09 | 135.15 | 176.55 |
101
+ | one process | warm hot | asynchronous | 322.53 | 48.66 | 70.59 | 75.87 |
102
+ | one process | warm many | synchronous | 119.05 | 132.25 | 237.52 | 247.05 |
103
+ | one process | warm many | asynchronous | 288.26 | 32.64 | 70.33 | 562.42 |
104
+ | one process | cold many | synchronous | 47.67 | 326.04 | 435.46 | 507.03 |
105
+ | one process | cold many | asynchronous | 99.39 | 148 | 236.03 | 295.26 |
106
+ | four processes | warm hot | synchronous | 518.66 | 30.15 | 52.73 | 78.57 |
107
+ | four processes | warm hot | asynchronous | 506.8 | 26.39 | 92.26 | 102.18 |
108
+ | four processes | warm many | synchronous | 190.36 | 76.65 | 145.7 | 158.94 |
109
+ | four processes | warm many | asynchronous | 77.12 | 194.6 | 399.43 | 527.1 |
110
+ | four processes | cold many | synchronous | 54.74 | 273.33 | 646.68 | 650.09 |
111
+ | four processes | cold many | asynchronous | 93.85 | 145.27 | 333.78 | 426.6 |
112
+
113
+ ### PostgreSQL 17.11
114
+
115
+ | Topology | Shape | Handler | ops/s | p50 ms | p95 ms | p99 ms |
116
+ | -------------- | --------- | ------------ | -----: | -----: | -----: | ------: |
117
+ | one process | warm hot | synchronous | 206.31 | 69.33 | 132.69 | 153.03 |
118
+ | one process | warm hot | asynchronous | 212.07 | 72.38 | 96.75 | 106.12 |
119
+ | one process | warm many | synchronous | 191.4 | 55.71 | 147.99 | 722.6 |
120
+ | one process | warm many | asynchronous | 66.6 | 121.61 | 920.97 | 2441.28 |
121
+ | one process | cold many | synchronous | 31.38 | 500.73 | 705.15 | 722.54 |
122
+ | one process | cold many | asynchronous | 17.24 | 909.02 | 1192.9 | 1211.35 |
123
+ | four processes | warm hot | synchronous | 265.63 | 57.94 | 82.52 | 109.46 |
124
+ | four processes | warm hot | asynchronous | 330.94 | 46.81 | 63.76 | 70.25 |
125
+ | four processes | warm many | synchronous | 161.73 | 58.67 | 237.04 | 718.55 |
126
+ | four processes | warm many | asynchronous | 119.63 | 68.71 | 531.12 | 1372.5 |
127
+ | four processes | cold many | synchronous | 50.84 | 314.51 | 487.09 | 506.45 |
128
+ | four processes | cold many | asynchronous | 29.93 | 546.7 | 615.36 | 638.1 |
129
+
130
+ ### MySQL 9.7.1
131
+
132
+ | Topology | Shape | Handler | ops/s | p50 ms | p95 ms | p99 ms |
133
+ | -------------- | --------- | ------------ | -----: | ------: | ------: | ------: |
134
+ | one process | warm hot | synchronous | 79.59 | 208.66 | 222.86 | 225.19 |
135
+ | one process | warm hot | asynchronous | 79.98 | 206.78 | 232.74 | 237.94 |
136
+ | one process | warm many | synchronous | 79.7 | 129.25 | 395.52 | 1987.46 |
137
+ | one process | warm many | asynchronous | 28.06 | 338.85 | 1992.54 | 6004.98 |
138
+ | one process | cold many | synchronous | 13.78 | 1215.91 | 1445.61 | 1470.91 |
139
+ | one process | cold many | asynchronous | 11.63 | 1347.74 | 1733.01 | 1820.26 |
140
+ | four processes | warm hot | synchronous | 213.75 | 72.85 | 94.48 | 106.73 |
141
+ | four processes | warm hot | asynchronous | 228.32 | 70.45 | 84.96 | 90.39 |
142
+ | four processes | warm many | synchronous | 105.31 | 90.36 | 297.88 | 1064.14 |
143
+ | four processes | warm many | asynchronous | 80.03 | 149.92 | 324.98 | 1090.95 |
144
+ | four processes | cold many | synchronous | 51.82 | 306.1 | 407.22 | 424.45 |
145
+ | four processes | cold many | asynchronous | 27.35 | 554.03 | 807.18 | 868.48 |
146
+
147
+ The cold and asynchronous cases keep the poorest throughput and the longest
148
+ tail. These are observed limitations. They are not capacity recommendations.
149
+ Repeat the runs on application-shaped payloads before you draw a general
150
+ conclusion. Integration tests cover PostgreSQL 14, MySQL 8.0, and other database
151
+ versions, but this harness did not measure them.
152
+
153
+ ### Virtualization cost
154
+
155
+ The same server version ran natively and in Docker Desktop on the same machine,
156
+ on the same day, through the same harness. Only the container boundary changes.
157
+
158
+ | Database | Topology and shape | Native ops/s | Docker ops/s | Native gain |
159
+ | ---------------- | ------------------------------- | -----------: | -----------: | ----------: |
160
+ | PostgreSQL 17.11 | four processes, warm hot, async | 330.94 | 67.69 | 4.9x |
161
+ | PostgreSQL 17.11 | one process, warm hot, sync | 206.31 | 56.44 | 3.7x |
162
+ | MySQL 9.7.1 | four processes, warm hot, async | 228.32 | 60.27 | 3.8x |
163
+ | MySQL 9.7.1 | one process, warm hot, sync | 79.59 | 44.56 | 1.8x |
164
+
165
+ Across the full matrix, Docker Desktop cost between 1.0x and 7.8x of the native
166
+ throughput. The multi-process rows lose the most, because more connections and
167
+ more commits cross the container boundary. Measure a database on the deployment
168
+ shape you intend to run, and state the boundary with any number you publish.
169
+
170
+ Earlier releases of this document reported PostgreSQL 18.4 and MySQL 8.4.11 in
171
+ Docker Desktop on the `0.13.0` tree. Those numbers measured the container as
172
+ much as the database, so the tables above replace them.
152
173
 
153
174
  ## Sources of bias
154
175
 
@@ -156,7 +177,8 @@ covered by integration tests but were not benchmarked.
156
177
  - Loopback database connections exclude production network latency.
157
178
  - Filesystem cache, SQLite WAL state, Node JIT warmup, and garbage collection
158
179
  affect short runs.
159
- - Docker Desktop adds virtualization overhead to containerized databases.
180
+ - Docker Desktop costs between 1.0x and 7.8x of the native throughput. The
181
+ tables above use native servers. See [Virtualization cost](#virtualization-cost).
160
182
  - The payload is a small counter, not a representative application state size.
161
183
  - The harness measures default durability settings and one client concurrency.
162
184
  - Hot-identity results deliberately include serialization and cannot be scaled
@@ -79,9 +79,9 @@ stale revisions within an incarnation.
79
79
 
80
80
  `SolidObjectsComponentRegistry` maps changed observable names to keyed UI
81
81
  registrations. The browser supplies an asynchronous `refresh` function and a
82
- synchronous `apply` function, so HTML, virtual DOM, and framework-native render
83
- results use the same coordination contract without assuming a rendering
84
- framework.
82
+ synchronous `apply` function. HTML, virtual DOM, and framework-native render
83
+ results therefore use the same coordination contract. The registry assumes no
84
+ render framework.
85
85
 
86
86
  Components may share a batch name. A microtask unions affected components in
87
87
  the same actor, batch, incarnation, and revision into one refresh request.
@@ -105,3 +105,62 @@ several WebSocket processes, the configured `broadcast` callback publishes the
105
105
  committed envelope through a shared transport and each process passes received
106
106
  envelopes to `runtime.realtime.publish()`. The session fence safely drops the
107
107
  duplicate seen by a process that both claimed and received the same event.
108
+
109
+ ## Tab host channel protocol
110
+
111
+ `solid-objects/browser/tab-host` uses a second, unrelated wire surface: a
112
+ `BroadcastChannel` between tabs of one origin. Every envelope carries
113
+ `protocol: "solid-objects-tab-host"` and `version: 1`; a listener ignores
114
+ anything else. Three kinds exist:
115
+
116
+ - `invoke`: a client request with a `requestId` (a UUID the client generates),
117
+ the target `actorType`, `actorId`, `operation`, and a JSON `arguments`
118
+ object.
119
+ - `result`: the leader's answer for one `requestId`, with either an `ok`
120
+ value or a named error.
121
+ - `leader-online`: the announcement a new leader posts on promotion. Clients
122
+ re-post their pending requests when they see it.
123
+
124
+ The client retries an `invoke` on an interval until a `result` arrives or its
125
+ timeout passes. The leader enqueues each request with `tab:<requestId>` as the
126
+ idempotency key, so a retried or re-posted request applies once. The channel
127
+ is same-origin plumbing between the application's own tabs; it carries no
128
+ authentication, so the trust boundary is the origin.
129
+
130
+ ## Sync envelope
131
+
132
+ `solid-objects/transmit` transmits one JSON envelope per staged transmit
133
+ effect: `effectId`, target `actorType` and `actorId`, `operation`, and an
134
+ optional `arguments` object that defaults to an empty object on both
135
+ ingests. The transport belongs to the host application; the
136
+ Playwright suite posts envelopes over `fetch`. The server calls
137
+ `receiveTransmitEnvelope`, which enqueues an internal message with
138
+ `transmit:<effectId>` as the idempotency key, so a replayed envelope applies once.
139
+ Internal delivery skips `authorizeMessage`; the host must authenticate the
140
+ sender before that call.
141
+
142
+ ## Shared database channel protocol
143
+
144
+ `solid-objects/database/shared-sqlite-wasm` uses a third wire surface: a
145
+ `BroadcastChannel` that carries SQL sessions from every tab to the current
146
+ database holder. Every envelope carries
147
+ `protocol: "solid-objects-shared-sqlite"` and `version: 1`. Seven kinds
148
+ exist:
149
+
150
+ - `ping` and `pong`: holder discovery. A new instance pings until a holder
151
+ answers with its `epoch`.
152
+ - `holder-online`: the announcement a new holder posts on promotion, with a
153
+ fresh `epoch`.
154
+ - `open`: start a session (`connection` or `transaction`) with a client
155
+ `sessionId`.
156
+ - `statement`: one `run`, `get`, `all`, or `now` operation inside a session.
157
+ - `close`: finish a session with `commit`, `rollback`, or `end`.
158
+ - `result`: the holder's answer for one `requestId`.
159
+
160
+ Every request carries the `epoch` it targets. A holder rejects requests from
161
+ another epoch, so a client learns about a failover from a fast rejection
162
+ rather than a timeout. A session that has not executed a statement retries
163
+ against the new holder automatically; later failures surface as
164
+ `SharedDatabaseFailover`, because a partially executed session must not
165
+ replay. The channel is same-origin plumbing with the origin as its trust
166
+ boundary, the same as the tab host protocol.
@@ -3,15 +3,16 @@
3
3
  This guide compares coordination models so an application can choose the
4
4
  smallest mechanism that meets its requirements. It does not rank the projects.
5
5
 
6
- | Approach | State and serialization unit | Deployment and durable substrate | Separate service | Replay versus state | Realtime and edge placement | Cross-identity transaction | Data access |
7
- | --------------------------- | ---------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | --------------------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------- |
8
- | SQL transaction or row lock | Rows selected by one transaction | Application process and SQL database | No | The application retries a failed transaction | Application-owned | Yes, for rows in the same database transaction | Ordinary application tables and SQL tools |
9
- | Traditional job queue | A job, queue, or configured grouping key | Workers plus broker or queue database | Usually | The job is retried; mutable entity state remains application-owned | Application-owned | Not supplied by the queue | Queue administration plus application data stores |
10
- | Solid Objects | Actor class and application-defined ID | Node processes plus existing SQLite, PostgreSQL, or MySQL | No; Redis wake-up is optional | The operation retries against durable actor state | Committed projections; application-owned transport; no edge placement | No | Relational tables, typed administration, CLI, and dashboard |
11
- | Cloudflare Durable Objects | Object class and globally unique ID | Cloudflare Workers plus per-object managed storage | Cloudflare platform | Object activation with durable state, not workflow-step replay | WebSockets and Cloudflare-selected object location | Storage transactions are scoped to one object | Object storage APIs and platform tooling |
12
- | Rivet Actors | Addressable actor | Rivet Engine or managed compute with actor state, KV, or per-actor SQLite | Rivet Engine | Actor persistence and lifecycle; workflows add recorded steps | Actor events and deployment-dependent placement | No general transaction across actors | Actor APIs and selected persistence model |
13
- | DBOS | Workflow ID and checkpointed steps | Application processes plus PostgreSQL system database | No orchestration server for the library; Conductor is recommended for distributed recovery | Deterministic workflow replay skips checkpointed steps | Workflow events; application placement | PostgreSQL transactions remain separate from workflow identity | PostgreSQL system database, client, CLI, and optional Conductor |
14
- | Restate | Service handler or keyed virtual object | Application services plus Restate's durable log and state store | Yes | Durable execution journals handler progress and object state | Service protocol and Restate deployment | No shared SQL transaction across object keys | Restate APIs, state tools, snapshots, and backups |
6
+ | Approach | State and serialization unit | Deployment and durable substrate | Separate service | Replay versus state | Realtime and edge placement | Cross-identity transaction | Data access |
7
+ | --------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------- | ------------------------------------------------------------------- |
8
+ | SQL transaction or row lock | Rows selected by one transaction | Application process and SQL database | No | The application retries a failed transaction | Application-owned | Yes, for rows in the same database transaction | Ordinary application tables and SQL tools |
9
+ | Traditional job queue | A job, queue, or configured grouping key | Workers plus broker or queue database | Usually | The job is retried; mutable entity state remains application-owned | Application-owned | Not supplied by the queue | Queue administration plus application data stores |
10
+ | Solid Objects | Actor class and application-defined ID | Node processes plus existing SQLite, PostgreSQL, or MySQL | No; Redis wake-up is optional | The operation retries against durable actor state | Committed projections; application-owned transport; no edge placement | No | Relational tables, typed administration, CLI, and dashboard |
11
+ | Cloudflare Durable Objects | Object class and globally unique ID | Cloudflare Workers plus per-object managed storage | Cloudflare platform | Object activation with durable state, not workflow-step replay | WebSockets and Cloudflare-selected object location | Storage transactions are scoped to one object | Object storage APIs and platform tooling |
12
+ | celld | Object class and object name | celld nodes plus one object-storage bucket; each object is its own SQLite database | Yes, the celld daemon on every node | The new owner restores the object's SQLite database from the bucket and resumes | Cloudflare Workers APIs; an object runs on one node of your fleet, not at an edge location | No | Per-object SQLite through the Workers storage APIs, plus the bucket |
13
+ | Rivet Actors | Addressable actor | Rivet Engine or managed compute with actor state, KV, or per-actor SQLite | Rivet Engine | Actor persistence and lifecycle; workflows add recorded steps | Actor events and deployment-dependent placement | No general transaction across actors | Actor APIs and selected persistence model |
14
+ | DBOS | Workflow ID and checkpointed steps | Application processes plus PostgreSQL system database | No orchestration server for the library; Conductor is recommended for distributed recovery | Deterministic workflow replay skips checkpointed steps | Workflow events; application placement | PostgreSQL transactions remain separate from workflow identity | PostgreSQL system database, client, CLI, and optional Conductor |
15
+ | Restate | Service handler or keyed virtual object | Application services plus Restate's durable log and state store | Yes | Durable execution journals handler progress and object state | Service protocol and Restate deployment | No shared SQL transaction across object keys | Restate APIs, state tools, snapshots, and backups |
15
16
 
16
17
  ## Primary references
17
18
 
@@ -22,6 +23,10 @@ smallest mechanism that meets its requirements. It does not rank the projects.
22
23
  distinguishes local concurrency from multiple worker processes.
23
24
  - Cloudflare documents global uniqueness, per-object storage, single-threaded
24
25
  execution, and placement in [What are Durable Objects?](https://developers.cloudflare.com/durable-objects/concepts/what-are-durable-objects/).
26
+ - celld documents per-object SQLite databases, bucket replication, and
27
+ object-storage compare-and-swap ownership in its
28
+ [repository](https://github.com/denoland/celld) and its
29
+ [documentation](https://celld.dev/docs).
25
30
  - Rivet documents addressable actors and persistence in
26
31
  [Actors](https://rivet.dev/docs/actors/) and
27
32
  [Persistence](https://rivet.dev/docs/actors/persistence).
@@ -59,8 +59,8 @@ runtime role enabled. Broadcast workers are started only when `broadcast` or
59
59
  Consecutive empty passes double it up to
60
60
  `idlePollingIntervalMilliseconds`. Actor workers never wait longer than
61
61
  `leaseRenewalIntervalMilliseconds`. Set the fast and idle values equal for a
62
- fixed cadence. A custom wake-up adapter should return `true` for a notification
63
- and `false` for a timeout; an older adapter that returns `void` remains
62
+ fixed cadence. A custom wake-up adapter must return `true` for a notification
63
+ and `false` for a timeout. An older adapter that returns `void` remains
64
64
  compatible and keeps the fast cadence. Wake-ups reduce latency, while database
65
65
  polling remains the correctness path.
66
66
 
@@ -12,12 +12,12 @@
12
12
  recreated actor. A caller authorized before destruction receives
13
13
  `ActorDestroyed`; an unknown or forged reference remains unauthorized.
14
14
  - Graceful process shutdown and stale-process cleanup use the same atomic
15
- ownership release: claimed messages return to ready membership, activations
16
- are unfenced, and processing effect, reminder, and broadcast claims become
17
- available again. A stale draining process is recoverable like a stale running
18
- process.
19
- - Permanent operation failure raises `MessageFailed` with the durable message
20
- ID and persisted error details instead of treating actor code text as the
15
+ ownership release. Claimed messages return to ready membership. The runtime
16
+ unfences the activations. Effect, reminder, and broadcast claims become
17
+ available again. A stale process that drains is recoverable like a stale
18
+ process that runs.
19
+ - Permanent operation failure raises `MessageFailed`. The error carries the
20
+ durable message ID and the persisted error details. Actor code text is not the
21
21
  public exception contract.
22
22
  - Operation, lifecycle, observable, and payload callbacks retain their owning
23
23
  runtime through async context. Isolated runtimes therefore never fall back to
@@ -31,14 +31,14 @@
31
31
  - Activation passes are bounded. Yielding changes ready-membership polling
32
32
  order only; it neither changes durable message sequence nor makes future work
33
33
  due early.
34
- - Idle hydrated actors remain fenced by the same renewable lease. Cache reuse
35
- never bypasses claim membership or the commit fence, failed turns restore
36
- their public fields before reuse, and conditional release cannot clear a
37
- newer owner or generation.
34
+ - Idle hydrated actors keep the same renewable lease as their fence. Cache reuse
35
+ never bypasses claim membership or the commit fence. A failed turn restores
36
+ its public fields before reuse. Conditional release cannot clear a newer owner
37
+ or generation.
38
38
  - Actor setup completes before an attempt begins. A hydration, migration, or
39
- activation failure atomically restores ready membership, releases its
40
- activation fence, and restores the attempt count; an awaiting caller receives
41
- the setup error.
39
+ activation failure restores ready membership, releases its activation fence,
40
+ and restores the attempt count. All three happen in one atomic step. A caller
41
+ that waits receives the setup error.
42
42
  - `guardApplicationDatabase()` rejects direct application writes during actor
43
43
  operations, observable and payload projections, and state migrations. It
44
44
  permits only `SELECT` through row-returning methods. Commit actions remain in
@@ -49,10 +49,10 @@
49
49
  successful snapshots and their nested JSON values are frozen copies.
50
50
  - Personalized payloads hydrate committed state separately for every payload
51
51
  name and subscriber. Each projection is read-only, size bounded, and fenced
52
- independently by actor incarnation and revision. One denied, mutating, or
53
- failing projection cannot stop its siblings or observable delivery. A state
54
- change on an actor declaring payloads creates a revision broadcast even when
55
- the actor declares no scalar observables.
52
+ independently by actor incarnation and revision. One projection that is
53
+ denied, that mutates, or that fails cannot stop its siblings or observable
54
+ delivery. A state change on an actor with payloads creates a revision
55
+ broadcast, even when that actor declares no scalar observables.
56
56
 
57
57
  ## Limitations and non-goals
58
58
 
@@ -71,6 +71,19 @@
71
71
  placement, capacity, database backups, and database failover.
72
72
  - Redis and PostgreSQL notifications reduce wake-up latency but do not replace
73
73
  durable polling or become a source of truth.
74
+ - The browser platform has no `AsyncLocalStorage`. Its context store covers
75
+ only the synchronous part of a callback. After the first `await` inside an
76
+ actor operation, the ambient guards (`applicationWritesForbidden()` and the
77
+ inside-transaction check) read as unset. Durable-state fencing, mailbox
78
+ ordering, and the SQLite WASM deadline enforcement do not depend on those
79
+ guards. The guards are best-effort in the browser and exact in Node.
80
+ - `snapshotWithIncarnation`'s `createdAtMs` orders actor incarnations to the
81
+ millisecond. Every adapter stores `created_at_ms` at that same precision. If
82
+ you destroy and recreate the same actor identity inside one database-clock
83
+ millisecond, the two incarnations get an equal `createdAtMs`. In that narrow
84
+ case, a caller cannot tell which of the two is current, so it cannot fence a
85
+ derived write on that value. `instanceId` still changes and shows that a
86
+ recreation occurred. It is a random UUID and carries no order of its own.
74
87
  - Large documents, bulk pipelines, globally placed edge state, and global
75
88
  counters are outside the intended workload. Prefer an ordinary row
76
89
  transaction when it completely enforces the invariant.
package/docs/dashboard.md CHANGED
@@ -115,9 +115,9 @@ with fresh random bytes, so tokens differ between requests while every form
115
115
  already open in the same session remains valid. POST requests without a valid
116
116
  token receive 403 and do not perform the action.
117
117
 
118
- Every stored or request-derived string is escaped before entering HTML,
119
- including JSON placed in chart attributes. HTML and statistics responses are
120
- private and not cached. The dashboard sends a nonce-backed content security
118
+ The dashboard escapes every stored or request-derived string before that string
119
+ enters the HTML. This includes the JSON in chart attributes. HTML and statistics
120
+ responses are private, and no cache holds them. The dashboard sends a nonce-backed content security
121
121
  policy, denies framing, disables MIME sniffing, and limits referrers to the
122
122
  same origin.
123
123
 
@@ -137,9 +137,9 @@ effects, broadcasts, and dead letters. Pause prevents workers from claiming
137
137
  new turns for that identity; a turn already executing may still commit. Resume
138
138
  clears the brake and normal polling resumes delivery.
139
139
 
140
- Dead-letter retry calls `runtime.deadLetters.retry()`, retaining its durable
141
- idempotency and actor-operation validation. A retry refused by the runtime is
142
- shown on the detail page with status 422.
140
+ Dead-letter retry calls `runtime.deadLetters.retry()`. It keeps the durable
141
+ idempotency and the actor-operation validation of that method. If the runtime
142
+ refuses a retry, the detail page shows it with status 422.
143
143
 
144
144
  `HEAD /` performs only a schema reachability query and creates no CSRF session
145
145
  state. Use it for liveness checks instead of polling the full dashboard.
@@ -19,18 +19,19 @@ do not parse error messages.
19
19
  | `PayloadTooLarge` | Arguments, state, result, snapshot getter, effect result, or personalized payload exceeded its configured limit. | Reduce the JSON value or deliberately raise the corresponding limit. |
20
20
  | `SyncInsideTransaction` | A committed call or message wait would self-deadlock inside this adapter's transaction. | Finish the transaction first or stage actor-owned work through a commit action. |
21
21
 
22
- `this.reject()` accepts codes matching `[A-Za-z_][A-Za-z0-9_]*`, including
23
- camelCase. An invalid code throws the non-retryable `InvalidRejectionCode`; the
24
- operation fails on its first attempt and a synchronous caller receives
25
- `MessageFailed` instead of waiting through retry backoff.
22
+ `this.reject()` accepts any code that matches `[A-Za-z_][A-Za-z0-9_]*`.
23
+ camelCase is valid. An invalid code throws the non-retryable
24
+ `InvalidRejectionCode`. The operation then fails on its first attempt, and a
25
+ synchronous caller receives `MessageFailed`. It does not wait through retry
26
+ backoff.
26
27
 
27
28
  `MessageReference.status()`, `result()`, and `wait()` reauthorize the stored
28
29
  operation. `result()` returns `undefined` while work is nonterminal, returns the
29
30
  committed result when complete, and raises `Rejected` or `MessageFailed` for a
30
31
  terminal refusal or failure. `wait()` blocks until the same terminal outcomes
31
- or its deadline. A reference does not retain the authorization context used to
32
- send it; supply the context to each of these methods so the stored operation is
33
- reauthorized.
32
+ or its deadline. A reference does not keep the authorization context of the
33
+ original send. Supply the context to each of these methods, so that the runtime
34
+ can reauthorize the stored operation.
34
35
 
35
36
  ## Definition and programming errors
36
37
 
@@ -97,8 +98,7 @@ linked replacement message; repeating the call returns the same replacement.
97
98
  Effects are different: they execute outside the actor transaction and are at
98
99
  least once. Deduplicate external work with the stable `EffectContext.id`.
99
100
  Success and failure callback operations receive the originally staged
100
- `arguments` for actor-state correlation. A retryable failure scheduled into the
101
- future is correctly considered idle for the present pass, so
102
- `runtime.testing.drain()` does not advance retry backoff; use a
103
- `NonRetryableError` when a test needs to exercise the exhausted failure callback
104
- without waiting.
101
+ `arguments` for actor-state correlation. A retryable failure with a future
102
+ schedule is correctly idle for the present pass, so `runtime.testing.drain()`
103
+ does not advance retry backoff. Use a `NonRetryableError` when a test must reach
104
+ the exhausted failure callback immediately.
package/docs/fit.md CHANGED
@@ -14,6 +14,9 @@ or realtime projections.
14
14
  invalidations atomically.
15
15
  - The application already operates SQLite, PostgreSQL, or MySQL and should keep
16
16
  durable coordination there.
17
+ - A local-first application needs the same actor model in the browser:
18
+ durable per-user state on SQLite WASM, one runtime shared across tabs, and
19
+ an outbox that syncs to a server runtime when the network allows.
17
20
 
18
21
  ## Prefer a row transaction when
19
22
 
@@ -13,9 +13,9 @@ not cross a process boundary. When live processes share the database without a
13
13
  configured adapter, the runtime logs
14
14
  `solid_objects.polling_only_cross_process_wake_up` once. Use PostgreSQL
15
15
  notifications or optional Redis Pub/Sub when separate processes need prompt
16
- delivery; without one, newly committed work can wait up to the current idle
17
- polling interval. Notification errors are isolated and logged by role and error
18
- class without failing the committed work.
16
+ delivery. Without one of them, newly committed work can wait for the current
17
+ idle polling interval. The runtime isolates notification errors and logs them by
18
+ role and error class. The committed work does not fail.
19
19
 
20
20
  The warning excludes process rows with the current hostname and host process ID.
21
21
  It can therefore appear during a rolling deployment or restart overlap when an
@@ -53,8 +53,8 @@ isolated backlogs. `solid_objects.activation.yielded` reports the actor
53
53
  identity, turns processed, and remaining due membership count.
54
54
 
55
55
  `claimScanLimit` defaults to 100. Global claims inspect a bounded ordered set of
56
- actor identities and continue after a lost lease race, preserving worker
57
- parallelism without an unbounded scan.
56
+ actor identities. They continue after a lost lease race. Worker parallelism
57
+ stays, and the scan stays bounded.
58
58
 
59
59
  Workers retain a hydrated actor and its fenced lease for
60
60
  `idleDeactivationTimeoutMilliseconds`, which defaults to 30 seconds. Idle
@@ -66,15 +66,17 @@ longer polling.
66
66
 
67
67
  Actors can override protected `onActivate()` and `onDeactivate()` methods for
68
68
  nondurable, process-local resources. Either hook may be asynchronous. Hook code
69
- runs under the application-write guard, and `onDeactivate()` is best effort:
70
- it may not run after a crash, cannot establish a correctness guarantee, and a
71
- failure is logged without preventing lease release.
69
+ runs under the application-write guard. `onDeactivate()` is best effort:
70
+
71
+ - it may not run after a crash;
72
+ - it cannot establish a correctness guarantee;
73
+ - the runtime logs a failure and still releases the lease.
72
74
 
73
75
  `runtime.administration.processes()` returns the same administration-authorized
74
76
  immutable process metadata as `runtime.processes.all()`, with hostname, host
75
- process ID, Node and Solid Objects versions, and a current `stale` flag. It is
76
- safe to call through the runtime's database adapter while workers are running;
77
- the query is serialized with other database access and does not require a
77
+ process ID, Node and Solid Objects versions, and a current `stale` flag. You can
78
+ safely call it through the runtime's database adapter while the workers run. The
79
+ runtime serializes the query with the other database access, and it needs no
78
80
  second SQLite connection. Graceful shutdown first persists `draining` with a
79
81
  `shutdownRequestedAt` timestamp, then deactivates owned actors and atomically
80
82
  releases every role claim before persisting `stopped`. `cleanup()` reauthorizes
@@ -82,7 +84,8 @@ separately and performs the same release for stale running or draining
82
84
  processes. The cleanup count is instrumented; application payloads are not.
83
85
 
84
86
  Committed calls and `message.wait()` apply `timeoutMilliseconds` to the entire
85
- durable wait, beginning before enqueue or message lookup. Adapter deadlines
87
+ durable wait. The clock starts before enqueue or message lookup. Adapter
88
+ deadlines
86
89
  bound serialized SQLite access and lock waits, PostgreSQL pool acquisition,
87
90
  statements, and locks, and MySQL pool acquisition, queries, and transaction
88
91
  lock waits. A `SyncEnqueueTimeout` means the enqueue transaction did not commit
@@ -112,10 +115,14 @@ ID and later calls return a reference to that same message.
112
115
 
113
116
  Self-scheduling actors need a low-frequency reconciler because application
114
117
  alarms can still be lost. `runtime.reconciliation` provides administration-
115
- authorized, read-only views for active instances, quiet instances without
116
- ready work, claimed work, or scheduled reminders, migrated state batches, and
117
- orphaned actor IDs. Collection reads use a maximum page size of 1,000 and a
118
- stable cursor.
118
+ authorized, read-only views for:
119
+
120
+ - active instances;
121
+ - quiet instances with no ready work, claimed work, or scheduled reminder;
122
+ - migrated state batches;
123
+ - orphaned actor IDs.
124
+
125
+ Collection reads use a maximum page size of 1,000 and a stable cursor.
119
126
 
120
127
  The host application supplies its current owner IDs to `orphaned()` because
121
128
  Node applications do not share an Active Record relation abstraction. Send
@@ -140,9 +147,16 @@ actor type appears in `instanceRetentionByActorType`, and remains an explicit
140
147
  operator action because it deletes the entire actor incarnation.
141
148
 
142
149
  Pruning selects and rechecks at most `pruneBatchSize` rows per transaction. It
143
- preserves ready and claimed messages, dead-letter originals and replacements,
144
- unfinished effects and broadcasts, scheduled reminders, leased or paused
145
- instances, and processes that still own a claim or activation. Instance
150
+ keeps:
151
+
152
+ - ready and claimed messages;
153
+ - dead-letter originals and replacements;
154
+ - unfinished effects and broadcasts;
155
+ - scheduled reminders;
156
+ - leased or paused instances;
157
+ - processes that still own a claim or an activation.
158
+
159
+ Instance
146
160
  expiration removes the entire actor incarnation and all of its retained
147
161
  history, so use it only for actor types whose state is safely disposable.
148
162