Skip to content

Latest commit

 

History

History
120 lines (96 loc) · 5.81 KB

durable-object-facets.md

File metadata and controls

120 lines (96 loc) · 5.81 KB

Durable Object Facets

A facet is a child object inside a Durable Object, with a SQLite database of its own. A supervisor class starts a facet from a class that a Worker Loader provides, so generated or untrusted code gets durable storage without a Durable Object namespace. Read the Cloudflare Durable Object Facets documentation for the standard API.

Example

The facets example creates a facet from a class that a Worker Loader provides.

API

  • ctx.facets.get(name, callback) returns a stub for the named facet. The callback returns { class, id }, and celld runs it only to start the facet.
  • worker.getDurableObjectClass("App") on a Worker Loader binding, or ctx.exports.App, supplies the class.
  • ctx.exports.App({ props }) creates a class handle with startup properties.
  • ctx.facets.abort(name, reason) stops a facet and keeps its database.
  • ctx.facets.delete(name) stops a facet and removes its database.
  • The stub answers fetch() and the RPC methods of the class.

A facet is a part of its root object

A facet is not a cell. It has no ownership record and no fencing epoch of its own, and it uses those of the root cell, which the Durable Objects page describes.

A facet runs on the owner node of the root cell. An eviction, a reset, and a move take the root object and its facets as one group, and the startup callback runs again at the next call.

As in workerd, a facet write commits in the database of the facet, so a rollback of a root transaction does not undo a facet call inside it. A facet and its root object therefore never commit atomically. celld holds an outbound effect of a facet, and the reply of a facet call, until the facet stream proves the writes of that call. The effect also passes the output gate of the root cell.

Starting and addressing a facet

ctx.facets.get(name, callback) returns a stub, and celld runs the callback only when the facet is not already running. The callback returns a class and an optional id. The class comes from worker.getDurableObjectClass("App") on a Worker Loader binding, or from ctx.exports.App for an exported DurableObject class without a storage migration; that facet runs in the isolate of its root.

A loaded class can extend DurableObject, or it can be a plain class with a (state, env) constructor. A plain class answers fetch(), and its RPC methods require the js_rpc compatibility flag, as on workerd.

id sets ctx.id in the facet. A DurableObjectId keeps its name, a string ID stays a string, and an omitted ID inherits the parent ID and its name. An unmigrated class handle has no idFromName(), so use a Durable Object namespace to create a named ID.

ctx.exports.App({ props }) creates a class handle with startup properties. The call copies the properties, so a later change to the object does not reach ctx.props. The properties must support structured cloning. A loopback class without properties supplies an empty object.

  • The name alone selects the database, so a changed id still reaches the stored data. A name has a limit of 256 bytes.
  • A facet can start facets of its own, to a total depth of 4 that counts the root object.
  • ctx.facets.abort(name, reason) stops a facet and keeps its database. A call on the stopped stub throws the reason.
  • ctx.facets.delete(name) stops the facet and removes its database and the database of every facet below it.

No name reaches a facet from outside its root object, so external traffic must go through the supervisor. Use a separate Durable Object, addressed with idFromName(), when children must run on different nodes.

WebSockets in a facet

A facet can accept a WebSocket: its fetch() returns a 101 response with the client end of a WebSocketPair, and the root object returns that response. The facet can call accept() on the server end or pass it to ctx.acceptWebSocket(). A facet can also open a WebSocket with new WebSocket(url) or with an Upgrade: websocket fetch. celld delivers the events of each such socket to the facet, not to the root object. Socket output waits for the facet stream and passes the output gate of the root cell.

A facet socket does not hibernate, because celld cannot start a facet again without its root object. A socket that the facet accepts or opens keeps the root cell resident until it closes. The client end of a 101 response from another Durable Object does not. getWebSockets() returns the sockets of that facet.

ctx.facets.abort() and ctx.facets.delete() close the sockets of each stopped facet with code 1001, and the facet receives no close event. workerd drops the connection instead, so its client sees 1006. A drain, an ownership move, or a generation swap closes a facet socket with 1012, and an output gate failure closes it with 1011.

Differences from Cloudflare

  • A Durable Object binding cannot supply a facet class. A class with a storage migration cannot supply one either, because ctx.exports holds that class as a namespace.
  • Each facet has a separate SQLite database, which celld replicates as a separate stream under the bucket prefix of the root Durable Object.
  • A facet cannot set an alarm. storage.setAlarm() throws inside a facet, so the root object must hold the schedule.
  • A facet socket does not hibernate. It keeps the root cell resident until the socket closes.
  • A facet stub is not awaitable, and a pipelined property path is unavailable, so a call must name one method.
  • The clone() method is unavailable.

The Cloudflare compatibility page lists the runtime APIs and the unsupported services.