diff --git a/CHANGELOG.md b/CHANGELOG.md index f1ad51a..a0b7133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## 0.14.3 - 2026-08-25 + +- Rewrite the first screen around the objection a reader actually has. The + README led with a shopping cart that appended to an array, which invites the + reply that one SQL statement already does it. It now leads with the ticket + sale from the homepage: 100 seats, a hold, a ten-minute expiry that frees the + seat, and a published count. That is the smallest example needing three + things from one number, and the three things are the argument. +- Answer "why not just use transactions?" in the first screen instead of + burying the fit sections. The section concedes the transaction and the row + lock first, including `navigator.locks` in the browser, then argues scope + rather than discipline: any `expiresAt` or `scheduledAt` column is evidence + the critical section already outlived the lock, and what follows it is a + sweeper and a race. +- Add "Is it worth installing here?", which names who should not install this, + and point readers with high-QPS reads or hot identities at + [Solid Objects Pro](https://solidobjects.pro/). +- Correct two claims. The realtime section said a value is published once per + change from the saving turn; the turn records the publication atomically, + while delivery is a separate worker and is at least once. Design provenance + said the API was redesigned around Web Components; there is no + `customElements` or `HTMLElement` in the package. +- Cut the README from 590 to about 540 lines by removing what `docs/` already + documented and what the page said three times, and add the table of contents + the standard-readme specification asks for above 100 lines. + ## 0.14.2 - 2026-08-24 - State that background pickup needs `runtime.run(signal)` diff --git a/README.md b/README.md index d557c5d..8df68c3 100644 --- a/README.md +++ b/README.md @@ -32,29 +32,69 @@ contract. See [Solid Objects in the browser](#solid-objects-in-the-browser). > data. > **Not a replacement for SQL transactions:** when one row update inside one -> transaction solves the problem, use that. Solid Objects earns its cost when an -> entity needs ordered calls across requests, retries, reminders, effects, and -> realtime state. See [Good and poor fits](#good-and-poor-fits). +> transaction solves the problem, use that and install nothing. Solid Objects +> earns its cost when the critical section outlives the transaction: a hold that +> expires in ten minutes, work that must survive a restart, or a fan-in that +> spans many jobs. See +> [Why not just use transactions?](#why-not-just-use-transactions). + +## Contents + +- [The programming model](#the-programming-model) +- [Why not just use transactions?](#why-not-just-use-transactions) +- [Is it worth installing here?](#is-it-worth-installing-here) +- [Run it now with SQLite](#run-it-now-with-sqlite) +- [What Solid Objects is for](#what-solid-objects-is-for) +- [Measured behavior](#measured-behavior) +- [Running in a deployed application](#running-in-a-deployed-application) +- [How it works](#how-it-works) +- [Delivery boundaries](#delivery-boundaries) +- [Realtime committed state](#realtime-committed-state) +- [Solid Objects in the browser](#solid-objects-in-the-browser) +- [Comparison](#comparison) +- [Requirements and supported systems](#requirements-and-supported-systems) +- [Operations](#operations) +- [Design provenance](#design-provenance) +- [Documentation](#documentation) +- [License](#license) ## The programming model +A ticket sale for one event, with 100 seats and a hold that expires: + ```typescript -import { Actor, configure } from "solid-objects" +import { Actor, broadcastValue, configure } from "solid-objects" import { sqlite } from "solid-objects/database/sqlite" -class Cart extends Actor { - static override readonly actorType = "Cart" +class TicketSale extends Actor { + static override readonly actorType = "TicketSale" + + remaining = 100 + holds: Record = {} - items: string[] = [] + override observables(): Record { + return { remaining: broadcastValue(this.remaining) } + } - add({ sku }: { sku: string }): number { - this.items.push(sku) - return this.items.length + reserve({ buyer }: { buyer: string }): boolean { + if (this.remaining === 0 || buyer in this.holds) return false + this.remaining -= 1 + this.holds = { ...this.holds, [buyer]: Date.now() } + this.schedule({ at: new Date(Date.now() + 600_000), key: buyer }).expire!({ buyer }) + return true + } + + expire({ buyer }: { buyer: string }): void { + if (!(buyer in this.holds)) return + const rest = { ...this.holds } + delete rest[buyer] + this.holds = rest + this.remaining += 1 } } const runtime = configure({ - database: sqlite({ path: "cart.sqlite3" }), + database: sqlite({ path: "tickets.sqlite3" }), authorizeMessage: () => true, authorizeQuery: () => true, }) @@ -62,16 +102,27 @@ const runtime = configure({ await runtime.install() try { - const cart = Cart.ref("cart-123") - await Promise.all([cart.add({ sku: "blue-shirt" }), cart.add({ sku: "green-hat" })]) + const sale = TicketSale.ref("event-42") + const buyers = ["ada", "grace", "alan"] + await Promise.all(buyers.map((buyer) => sale.reserve({ buyer }))) } finally { await runtime.close() } ``` -Both calls enter the durable mailbox for `cart-123`. They execute in order and -commit one state transition at a time, even when different requests or Node.js -processes submit them concurrently. +Every reserve enters the durable mailbox for `event-42`. They execute in order +and commit one state transition at a time, even when different requests or +Node.js processes submit them concurrently, so the guard on `remaining` cannot +oversell. + +That example wants three things from the same number. It must never go below +zero. It must give the seat back if the buyer does not pay within ten minutes. +It must show the current count to everyone watching the page. + +The first is one UPDATE statement. The second is an `expiresAt` column plus a +sweeper. The third is a push on every code path that changes the number. The +combination is what costs, not any one of them. Here the guard, the ten-minute +alarm, and the published count are one class, and they commit together. `install()` prepares the database and starts nothing. The example above finishes because the caller's own path executes each call. A process serves background @@ -85,28 +136,77 @@ process.on("SIGTERM", () => controller.abort()) await runtime.run(controller.signal) ``` +## Why not just use transactions? + +Often you should. If the whole job is read a row, decide, write it back, and +answer the request, then a transaction with `SELECT ... FOR UPDATE` does that +and you need nothing else installed. In the browser, `navigator.locks` is the +same answer. Reach for those first. + +The argument for an actor is scope, not discipline. A lock is scoped to one +transaction, on one connection, in one process. The ticket sale above leaves +that scope on one line: the hold expires in ten minutes, and no transaction +stays open for ten minutes. A `setTimeout` does not cover it either, because it +dies with the process. + +Any column named `expiresAt`, `scheduledAt`, or `nextRunAt` is evidence that +the critical section already outlived the lock that was supposed to cover it. +What follows such a column is a sweeper that looks for due rows, and then a +race between that sweeper and the next writer of the same row. The column, the +sweeper, and the race are what a Solid Objects actor replaces. + +Three cases a lock cannot reach: + +- work that fires at a future moment, when no transaction of yours is open; +- work that must survive a process restart, which rules out an in-process + timer; and +- a fan-in whose critical section spans many jobs over minutes, such as an + import that counts its own chunks as each one finishes. + +If it all happens inside one request, use a lock. If something has to happen +later, or has to survive a restart, that is when this is worth installing. + +## Is it worth installing here? + +Worth it when several requests, jobs, or processes act on the same cart, room, +device, event, or session, and each next action needs the last committed state. +Worth it when that same thing also owns work that fires later, or a number a +live page must show: per-document reminders and realtime projections of +committed state are the same argument. + +Not worth it for a plain counter, a single-row update inside one transaction, a +stateless job, bulk ingestion or a data-parallel pipeline, CPU-heavy work, a +large JSON document that belongs in normalized rows, globally placed edge +state, or a global rate-limit counter that every request touches. One hot +identity is serialized on purpose, so making everything one identity makes a +queue. Split an identity only when the domain can tolerate independent +ordering and transactions. + +High-QPS reads and hot identities are where this runtime stops being the right +tool on its own. [Solid Objects Pro](https://solidobjects.pro/) is a commercial +performance layer for the family that adds grouped commits, which coalesce +concurrent writes into fewer database commits; optional ephemeral operations, +which take loss-tolerant calls out of the durable journal; and materialized +projections, which build read models after commit so reads stop competing with +mailbox work. It ships for the Rails gem today, and the Node build is in +development. + +[What Solid Objects is for](#what-solid-objects-is-for) has the pattern table, +and [Choosing Solid Objects](docs/fit.md) is the longer guide. + ## Run it now with SQLite -Node.js 24.4.0 or newer is required. Node.js 24.15 or newer is preferred, -because `node:sqlite` prints an experimental warning before it. The published -package includes a quickstart: +The published package includes a quickstart that needs no checkout, database +server, container, or configuration: ```bash npm exec --yes --package=solid-objects@latest -- solid-objects quickstart ``` -The command needs no repository checkout, database server, Redis, container, or -application configuration. It uses Node's built-in SQLite module and removes -its scoped temporary database before exiting. - -It states its plan first, prints the `Counter` class it runs, and asks for -permission. It executes the work only after you answer, and then it explains -what each result proves. It asks nothing when stdin is not a terminal, so CI -never waits. Add `--yes` to skip the question in a terminal, or `--json` for a -machine-readable summary. - -The executable asserts rather than merely printing a plausible result. It exits -with a non-zero code when one of those checks fails. +It states its plan, prints the `Counter` class it runs, and asks before doing +anything. It asserts rather than printing a plausible result, and exits +non-zero when a check fails. Add `--yes` to skip the question, or `--json` for +a machine-readable summary. ## What Solid Objects is for @@ -133,38 +233,25 @@ prefer that. See [Choosing Solid Objects](docs/fit.md) for the longer guide. ## Measured behavior -One developer machine, not a capacity promise. Apple M5, Node.js 24.18.0, 250 -measured operations at client concurrency 16, on August 22, 2026. PostgreSQL -17.11 and MySQL 9.7.1 run natively, not in a container. +One developer machine, not a capacity promise: Apple M5, Node.js 24.18.0, 250 +operations at client concurrency 16, on August 22, 2026. | Measurement | Result | | ------------------------------------------------------------- | ---------------: | | Committed operations per second, one hot identity, SQLite | 286 to 323 ops/s | | The same identity across four processes, SQLite | 507 to 519 ops/s | -| The same identity across four processes, PostgreSQL | 266 to 331 ops/s | -| The same identity across four processes, MySQL | 214 to 228 ops/s | | Idle wake-up to committed result, one process | 2.66 ms p50 | | Idle wake-up to committed result, two processes, polling only | 1,006 ms p50 | -| Idle CPU per process, 100 ms fast interval | 0.121% | -| Idle database passes per second, after backoff | 4.0 | - -The four idle rows come from a separate harness on August 16, 2026. - -Each range spans the synchronous and the asynchronous handler shape. Calls to -one identity are serialized on purpose, so the per-call latency in these runs -includes the wait behind the other fifteen concurrent callers. Throughput is -the honest number for that case. -The same PostgreSQL and MySQL versions in Docker Desktop reached 1.8x to 4.9x -less throughput on those rows. Measure your own deployment shape before you -plan capacity. +Calls to one identity are serialized on purpose, so per-call latency includes +the wait behind the other fifteen callers. That last row is the tradeoff to +know before deploying: use PostgreSQL notifications or the optional Redis +Pub/Sub when separate processes need low-latency delivery. The same databases +in Docker Desktop reached 1.8x to 4.9x less throughput. -The polling-only row is the tradeoff to know before you deploy: use PostgreSQL -notifications or the optional Redis Pub/Sub when separate processes need -low-latency delivery. - -Conditions, sources of bias, and the complete matrix for all three databases -are in [Benchmarks](docs/benchmarks.md). +PostgreSQL and MySQL numbers, idle CPU, sources of bias, and the full matrix +are in [Benchmarks](docs/benchmarks.md). Measure your own deployment shape +before planning capacity. ## Running in a deployed application @@ -203,74 +290,59 @@ with your own workload. ## How it works -Solid Objects addresses an object by its TypeScript class and its -application-defined ID. Public fields are JSON state, public methods are durable -operations, and public getters are ordered queries. - -For each identity, Solid Objects: - -1. commits calls to a durable per-ID mailbox; -2. claims one activation with a renewable lease; -3. executes one operation at a time outside the database transaction; -4. commits state, completion, and staged work in a short fenced transaction; -5. retries recoverable failures and exposes terminal failures as dead letters; -6. publishes committed realtime invalidations in revision order. +An actor is addressed by its class and an application-chosen ID. Fields are +durable state, methods are operations, and getters are queries. One operation +is one turn: the runtime claims the actor under a fenced lease, runs your +JavaScript outside the transaction, then commits state, results, staged +effects, reminders, and realtime invalidations together. A failure retries with +backoff and dead-letters at the limit. The fence includes the activation owner, token, generation, expiration, and -claimed message. A worker that finishes JavaScript after losing its lease -cannot commit. See the executable [failure-recovery demonstration](examples/failure-recovery/demo.ts) -and the full [architecture](docs/architecture.md). - -Redis is optional wake-up infrastructure. It can reduce notification latency in -a multi-process MySQL deployment. The relational database stays the durable -source of truth, and polling stays the recovery path. - -Idle roles back off from the configured 100 ms fast polling interval to one -second. Processed work and wake-up notifications reset that interval -immediately. The default wake-up reaches only the current Node process; use the -PostgreSQL or optional Redis adapter when separate processes need low-latency -delivery. The runtime warns once when it sees that topology without an adapter. - -## Good and poor fits - -| Good fit | Poor fit | -| --------------------------------------------------------- | --------------------------------------------------------- | -| Multiplayer rooms and collaborative sessions | A single-row update already solved by one SQL transaction | -| Shopping carts, accounts, devices, and per-user workflows | Bulk ingestion and data-parallel pipelines | -| Stateful agent sessions with ordered tool results | Very high-throughput global counters | -| Per-document or per-device reminders | Large JSON documents that should remain normalized rows | -| Realtime projections of committed state | Globally placed edge state or managed elastic placement | +claimed message, so a worker that finishes its JavaScript after losing the +lease cannot commit. The database is the source of truth. Redis is optional +acceleration, and polling remains the recovery path. -One hot identity is intentionally serialized. Split an identity only when the -domain can tolerate independent ordering and transactions. Solid Objects does -not provide a transaction across object identities. - -The longer decision guide is in [Choosing Solid Objects](docs/fit.md). +[`examples/failure-recovery/demo.ts`](examples/failure-recovery/demo.ts) kills a +worker mid-turn and shows the survivor finish it. +[Architecture](docs/architecture.md) and +[Operations](docs/operations.md) cover the turn lifecycle, polling backoff, and +wake-up adapters. ## Delivery boundaries - Operations are ordered per identity and execute **at least once**. -- A crash after arbitrary external I/O but before the database commit can cause - that I/O to repeat. Use the stable effect ID or another durable idempotency - key at the external system. -- Fencing protects the Solid Objects database commit. It cannot undo an HTTP - request, email, payment, file write, or other external side effect. -- Different identities can execute concurrently; one hot identity cannot. -- State, result, actor-to-actor delivery, reminders, effects, commit actions, - and realtime invalidations commit together for one operation. +- Fencing protects the database commit. It cannot undo an HTTP request, email, + payment, or file write, so external systems need the stable effect ID or + another durable idempotency key. +- Different identities run concurrently; one hot identity cannot. - Cross-object transactions are not provided. -- Application processes with incompatible `stateVersion` values must not run - together. Older code rejects state written by a newer version. -- Direct application-database writes are guarded only when the application - uses the supplied database facade. Unwrapped clients cannot be intercepted. -- Realtime sessions are process-local. A multi-process application must bridge - committed broadcast events to the processes holding live connections. +- Realtime sessions are process-local, so a multi-process application must + bridge committed broadcast events to the processes holding live connections. -See [Correctness and delivery semantics](docs/correctness.md) and -[Errors and recovery](docs/errors-and-recovery.md) for the complete contract. +[Correctness and delivery semantics](docs/correctness.md) has the complete +contract, including `stateVersion` compatibility and the database write guard, +and [Errors and recovery](docs/errors-and-recovery.md) covers failure handling. ## Realtime committed state +For a like count or a dashboard number, write the row and then send on your own +socket. That is less code than this library and it works. + +It gets harder when several people write to the same record at once. Each +request builds its payload in its own process and sends it. The lock decided +who wrote first, but it has no say over which of the two sends arrives last, so +a viewer can be left looking at the older number. The second gap is that the +send is not part of the write: if the process dies after the database commits +and before the send goes out, the tab keeps a wrong number and nothing corrects +it. + +An observable is the alternative. The change and its publication commit +together, so no crash can leave one without the other. A worker delivers the +publication afterwards, claiming rows in actor revision order, and subscribers +reject a duplicate or stale revision. Delivery is still at least once, so the +guarantee is that a subscriber cannot end up on an older value, not that a +value is sent exactly once. + Actors opt into browser-visible dependencies. In `0.13`, an unwrapped observable triggers invalidation without storing or sending its value. Use `broadcastValue()` only for a scalar that every authorized subscriber may see: @@ -333,33 +405,27 @@ await runtime.install() await Counter.ref("page-hits").increment() ``` -That code runs identically in every tab. `sharedSqliteWasm` elects one -database holder per origin through the Web Locks API, carries the other -tabs' SQL to it over a `BroadcastChannel`, and fails over onto the same -durable state when the holder's tab dies. Use `sqliteWasm` directly for a -single dedicated worker. - -Two companions complete the local-first story: +That code runs identically in every tab. `sharedSqliteWasm` elects one database +holder per origin through the Web Locks API, carries the other tabs' SQL to it +over a `BroadcastChannel`, and fails over onto the same durable state when the +holder's tab dies. Use `sqliteWasm` for a single dedicated worker. -- `solid-objects/browser/tab-host` runs one runtime for all tabs when the - application prefers request-level routing: the leader's worker executes - every operation, and other tabs invoke through a `BroadcastChannel` client - by name. -- `solid-objects/transmit` drains the transactional effects outbox to a - server with at-least-once delivery, per-actor order, and an idempotent - server ingest, so offline writes reconcile when the network returns. +Two companions complete the local-first story: `solid-objects/browser/tab-host` +runs one runtime for all tabs when the application prefers request-level +routing, and `solid-objects/transmit` drains the transactional effects outbox +to a server with at-least-once delivery, per-actor order, and an idempotent +server ingest, so offline writes reconcile when the network returns. ### The backend can be Rails, not only Node -The browser runtime does not require a Node server behind it. The transmit -wire contract is shared with the Ruby gem +The browser runtime does not require a Node server behind it. The transmit wire +contract is shared with the Ruby gem ([solid-objects-ruby](https://github.com/cardmagic/solid-objects-ruby)): `SolidObjects::Transmission.receive` accepts the same envelopes as the Node -ingest `receiveTransmitEnvelope`, dedups on the same `transmit:` -key, and both repositories pin the contract with one shared fixture file. -A browser front end on `solid-objects/browser/host` inside a Rails -application therefore replays its offline writes directly onto Ruby server -actors — no Node service in between: +ingest `receiveTransmitEnvelope`, dedups on the same `transmit:` key, +and both repositories pin the contract with one shared fixture file. A browser +front end therefore replays its offline writes directly onto Ruby server +actors, with no Node service in between: ```ruby class TransmitController < ApplicationController @@ -383,65 +449,51 @@ The wire shapes are documented in the ## Comparison -These systems solve different coordination problems. The table describes their -default unit and deployment model, not a quality ranking. - -| Approach | Serialization and state unit | Durable substrate | Additional runtime | Recovery model | Placement | -| --------------------------- | ----------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------- | -| SQL transaction or row lock | Selected rows in one transaction | Application database | None | Application retries the transaction | Application deployment | -| Traditional job queue | Job or queue; ordering depends on queue configuration | Broker or queue database | Queue workers and usually a broker | Retry the job | Application deployment | -| Solid Objects | TypeScript class plus object ID | Existing SQLite, PostgreSQL, or MySQL | Library in application processes | Retry the per-ID operation from durable state | Application deployment | -| Cloudflare Durable Objects | Object class plus globally unique ID | Per-object managed storage | Cloudflare Workers platform | Managed object activation | Cloudflare-selected location | -| celld | Object class plus object name | Per-object SQLite replicated to a bucket you own | celld daemon that embeds V8 and runs Wrangler bundles | A new owner restores the object database from the bucket | Any node in your fleet, chosen by bucket compare-and-swap | -| Rivet Actors | Addressable actor | Actor state, KV, or per-actor SQLite | Rivet Engine or managed compute | Actor sleep, wake, and persistence | Configured Rivet deployment | -| DBOS | Workflow ID and checkpointed steps | PostgreSQL system database | Library; Conductor recommended for distributed recovery | Deterministic workflow replay from checkpoints | Application deployment | -| Restate | Service handler or keyed virtual object | Restate log and state store | Restate server or cloud service | Durable handler execution and journal replay | Restate deployment | - -celld and Solid Objects both self-host the Durable Objects model. The difference -is where the state lives and what you run. celld runs a daemon that embeds V8 -and executes Wrangler bundles. It gives each object its own SQLite database, -and it replicates that database to an object-storage bucket you own. Object -ownership moves between nodes through compare-and-swap on that bucket. Solid -Objects runs plain TypeScript classes inside your Node processes, adds no -daemon, and keeps object state in the SQL database the application already -operates. Choose celld to run Workers-format code across a fleet with -bucket-based placement. Choose Solid Objects to keep one database, no extra -process, and an ordinary Node deployment. - -[docs/comparisons.md](docs/comparisons.md) holds the sourced comparison for each -dimension: realtime projections, edge placement, cross-identity transactions, -and operational data access. +A SQL transaction or row lock serializes selected rows for one transaction and +needs nothing installed. A job queue retries a job but leaves ordering to queue +configuration. Solid Objects serializes by object ID, keeps state in the +database you already run, and adds a library rather than a service. Cloudflare +Durable Objects, Rivet, DBOS, and Restate each add a managed runtime or server. + +celld and Solid Objects both self-host the Durable Objects model, and the +difference is what you run. celld runs a daemon that embeds V8 and executes +Wrangler bundles, gives each object its own SQLite database, replicates that +database to an object-storage bucket you own, and moves ownership between nodes +through compare-and-swap on that bucket. Solid Objects runs plain TypeScript +classes inside your Node processes, adds no daemon, and keeps object state in +the SQL database the application already operates. Choose celld to run +Workers-format code across a fleet with bucket-based placement. Choose Solid +Objects to keep one database, no extra process, and an ordinary Node +deployment. + +[docs/comparisons.md](docs/comparisons.md) has the full table across eight +systems, with primary sources for every dimension: realtime projections, edge +placement, cross-identity transactions, and operational data access. ## Requirements and supported systems -- Node.js 24.4.0 or newer; 24.15 or newer to avoid the `node:sqlite` - experimental warning -- TypeScript 5.9 or newer for TypeScript applications -- SQLite through `node:sqlite`, PostgreSQL 14 or newer, or MySQL 8.0 or newer - with InnoDB -- optional `pg`, `mysql2`, `redis`, or `@sqlite.org/sqlite-wasm` peer - dependency only for the selected adapter -- for the browser runtime: a browser with OPFS for persistent storage and the - Web Locks API for the multi-tab host +Node.js 24.4.0 or newer, TypeScript 5.9 or newer, and SQLite through +`node:sqlite`, PostgreSQL 14 or newer, or MySQL 8.0 or newer on InnoDB. +`pg`, `mysql2`, `ioredis`, and `@sqlite.org/sqlite-wasm` are optional peer +dependencies, installed only for the adapters you use. -[Supported versions](docs/support.md) records the exact CI matrix and the -boundaries. +[Supported versions](docs/support.md) records the exact CI matrix, the browser +requirements for OPFS and the Web Locks API, and the boundaries. ## Operations -`runtime.run(signal)` supervises actor, effect, reminder, broadcast, retention, -and stale-process recovery roles. The database-backed operator dashboard is an -optional `solid-objects/web` export with deny-by-default administration policy, -session-backed CSRF protection, and Fetch or Node/Connect mounting. - -The dashboard defaults to authorized read/write access. An authorized read-only -mode removes the mutations. Use the explicitly public read-only mode only for -synthetic demo data, because it exposes stored arguments, results, errors, -identifiers, and operational metadata. +`runtime.run(signal)` supervises the actor, effect, reminder, broadcast, +retention, and stale-process recovery roles. An optional `solid-objects/web` +export mounts a database-backed operator dashboard behind a deny-by-default +administration policy, with session-backed CSRF protection and Fetch or +Node/Connect mounting. Administration is also available through the JSON CLI +and typed runtime managers. -Administration remains available through the JSON CLI and typed runtime -managers. See [Operations](docs/operations.md), the [dashboard guide](docs/dashboard.md), -and [Configuration](docs/configuration.md). +The dashboard has an explicitly public read-only mode. Use it only for +synthetic demo data: it exposes stored arguments, results, errors, identifiers, +and operational metadata. See [Operations](docs/operations.md), the +[dashboard guide](docs/dashboard.md), and +[Configuration](docs/configuration.md). ## Design provenance @@ -453,8 +505,8 @@ twelve earlier JavaScript release generations. The TypeScript implementation is not a source translation. It redesigned the API around inferred TypeScript references, Node runtime supervision, -`node:sqlite`/`pg`/`mysql2` adapters, transport-neutral realtime sessions, -Web Components, and browser-safe package exports. The +`node:sqlite`/`pg`/`mysql2` adapters, transport-neutral realtime sessions, a +framework-neutral component registry, and browser-safe package exports. The [parity ledger](docs/parity.md) records capability relationships and deliberate runtime differences. diff --git a/package.json b/package.json index 62824aa..0814922 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "solid-objects", - "version": "0.14.2", + "version": "0.14.3", "description": "Race-free realtime state per application identity, backed by your SQL database", "type": "module", "license": "MIT", diff --git a/src/version.ts b/src/version.ts index ded049d..0d29b78 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = "0.14.2" +export const VERSION = "0.14.3"