Skip to content

Continuity and projection ​

Continuity answers a different question from projection. Continuity defines which page instance may continue a task and which bounded checkpoint is shared. Projection defines how controls across Shadow DOM and same-origin iframe boundaries participate in one page capability surface. Mixing them produces the wrong security model: a checkpoint is not an execution lease, and a visible iframe is not automatically shared authority.

Continuity scopes ​

@plingjs/agent exposes three scopes:

ScopeStorage and arbitrationIntended use
tabBrowser sessionStorage, unless a custom ContinuityStore is suppliedMPA navigation in the same tab
browserA same-origin browser authority; localStorage and lease coordination by defaultMultiple tabs that may follow or explicitly take over one task
remoteA host-provided ContinuityAuthority, such as @plingjs/continuity-httpDifferent devices or browsers

scope defaults to tab. remote requires an explicit authority; there is no hidden fallback to local storage.

ts
createPling({
  continuity: {
    scope: "remote",
    authority: createHttpContinuityAuthority({ baseUrl: "/pling/continuity" }),
  },
});

Checkpoint bounds ​

OptionDefaultMeaning
ttlMs2 hoursA checkpoint older than this is not restored
maxMessages20Maximum retained conversation messages
maxEvents60Maximum retained UI events
maxChars4000Maximum characters for one string or argument
leaseTtlMs30 secondsExecution-owner lease duration
takeover"manual"Only an explicit human action may preempt an active owner
maxTasks20Shared-task discovery limit
resume"ask"User-facing resume policy
returnOnDonedisabledSame-origin return after task completion; a delay is capped at five seconds

The checkpoint contains a bounded task summary, recent messages, recent events, page identity, and status. It does not contain DOM, screenshots, cookies, JWTs, provider keys, or arbitrary application state.

Authority operations ​

ContinuityAuthority is the transport-independent port:

ts
interface ContinuityAuthority {
  list(): Promise<ContinuityTaskSummary[]>;
  read(sessionId: string): Promise<ResidencyEnvelope | null>;
  create(input: CreateTaskInput): Promise<LeaseGrant>;
  claim(input: ClaimTaskInput): Promise<LeaseGrant>;
  renew(input: RenewLeaseInput): Promise<LeaseGrant>;
  release(input: ReleaseLeaseInput): Promise<void>;
  compareAndSwap(input: CompareAndSwapInput): Promise<ResidencyEnvelope>;
  remove(input: ReleaseLeaseInput): Promise<void>;
  subscribe?(listener: (change) => void): () => void;
  dispose?(): void;
}

The backend must atomically check the revision and lease. A stale writer is rejected with REVISION_CONFLICT; it must not overwrite a newer checkpoint. preempt: true is reserved for an explicit user takeover. Automatic resume may join as a follower, but it must not seize an active owner.

ContinuityChange is notification metadata only: type, session id, revision, status, owner/lease identifiers, lease expiry, and time. It is not a second authority. A lost notification may delay a UI update, but every write still checks the current lease and revision.

HTTP integration ​

@plingjs/continuity-http provides both sides:

  • createHttpContinuityAuthority(options) for the browser;
  • createContinuityHandler({ backend, identity, ... }) for a Web Request → Response backend.

The default base path is /pling/continuity.

Method and pathOperation
GET /healthHealth
GET /sessionResolve identity
GET /eventsSSE lease and revision metadata
GET /tasksList the current user's tasks
POST /tasksCreate a task and lease
GET /tasks/:idRead a checkpoint
PUT /tasks/:idCompare-and-swap a checkpoint
DELETE /tasks/:idOwner-only removal
POST /tasks/:id/claimClaim or explicitly take over
POST /tasks/:id/renewRenew the lease
POST /tasks/:id/releaseRelease the lease

userId comes only from verified identity. It is never read from the request body. The handler maps stable backend errors to HTTP status:

CodeHTTP
UNAUTHORIZED401
OWNER_MISMATCH403
NOT_FOUND404
TASK_EXISTS, REVISION_CONFLICT, LEASE_HELD, LEASE_REVOKED409
LEASE_EXPIRED410
INVALID_STATE422
STORE_UNAVAILABLE500

Network failure, 401, and an unprovable lease fail closed. The page does not execute merely because it has a remembered summary.

Projection across boundaries ​

Shadow DOM ​

Shadow roots change the parent traversal rule, not the action model. Declarations stay on the element, and the DOM layer uses a composed tree so records, scopes, availability, and feedback can cross the boundary. There is no second registry for a shadow root.

Same-origin iframe ​

An iframe is not scanned or adopted automatically. The parent explicitly calls:

ts
import { sharePlingPageWithFrame } from "@plingjs/dom";

const stop = sharePlingPageWithFrame(frame, page);

The child can read or wait for that runtime:

ts
import { plingPageFromParent, waitForPlingPageFromParent } from "@plingjs/dom";

const existing = plingPageFromParent();
const page = existing ?? await waitForPlingPageFromParent({ timeoutMs: 5000 });

This shared-memory path requires same-origin access. A cross-origin frame must use an explicit postMessage or Surface transport with its own identity and permission checks. Reading contentWindow failure is not an authorization result.

When embedding distinct framework islands, give each adapter a short namespace. IDs are prefixed with namespace/ before appearing in actions, scopes, records, state, or feedback. Namespace collisions are visible in the full id, not hidden by separate registries.

What not to infer ​

  • A projected control is not automatically safe to expose. Exposure remains the expose and session-scope intersection.
  • A checkpoint does not grant execution rights. A valid lease does.
  • A visible cross-origin iframe is not a shared runtime.
  • An SSE message is not durable task state.
  • Browser localStorage coordination is not a multi-device authority.

Verification ​

Run the backend conformance suite against a disposable namespace:

ts
import { assertContinuityBackendConformance } from "@plingjs/continuity-http/testing";

await assertContinuityBackendConformance(productionBackend, {
  expiryTtlMs: 1_100,
  advanceTime: () => clock.advance(1_200),
});

It checks user isolation, duplicate creation, non-preempting claims, takeover, stale compare-and-swap, lease renewal, release and reclaim, owner-only removal, and expiry. The included memory backend is a testing reference, not a production store.

Relevant examples cover MPA continuity, Shadow DOM projection, same-origin iframe projection, cross-tab follow and takeover, cross-tab forks, cross-device continuity, and a server-side continuity store.

Version and compatibility ​

ItemValue
Applies to@plingjs/continuity-http 0.1.0-alpha.2
Introduced0.1.0-alpha.2 (first published alpha)
RuntimeBrowsers with ES2022 modules; Node.js 20 or later for the HTTP backend
Peer dependenciesNone
StabilityPre-1.0 alpha; a minor release may change the API

Continuity backends implement the ports in this package; the memory backend is only a test fixture. Compatibility therefore includes both the package version and the backend's conformance result.