Skip to content

ViewHost and service boundaries

ViewHost is an application composition facade. Optional services/methods enable capabilities independently; resolveSource is required. It does not prescribe one REST controller or URL layout.

ServiceMethodResult / responsibility
definitionload(definitionId, signal?)Promise<ViewDefinition>
instancelist(definitionId, signal?)Promise<ViewInstanceList>
instanceload(instanceId, signal?)Promise<ViewInstance>
instancecreate(instanceWithoutIdOrRevision, context)Authoritative created instance; context carries requestId and optional signal
instancesave(instance)Saved instance with authoritative revision
instancerename(instanceId, title, revision)Renamed instance
instancedelete(instanceId, revision)Promise<ViewDeleteResult>
preferencesaveOrder(definitionId, instanceIds)Persist the current user's ordering
preferencesaveDefault(definitionId, instanceId)Persist the current user's default; instanceId is string | null
permissiongetInstance(instance)Synchronous ViewInstancePermissions
permissiongetDefinition()Synchronous { reorder } projection
permissionload(definitionId, signal?)Initialize getters; resolve a ViewPermissionSnapshot
permissionrefresh(signal?), subscribe(listener)Refresh grants; return an unsubscribe function from subscribe
hostresolveSource(sourceId)A ViewSource or Promise of one; record paging or analysis aggregate

Engine loading awaits permission.load, falling back to permission.refresh when load is absent. Permission initialization failure prevents ready state. A getter must be pure and expose the initialized policy. Notify subsequent changes or replace the host; mutating an invisible closure does not notify React.

ViewDeleteResult has the required field defaultInstance: ViewInstance | null: the calling user's authoritative default from the deletion transaction. Idempotent repeats also return this receipt; null explicitly means no default. The engine validates and adopts its ID rather than inferring from stale local order or making an extra list request. It initializes a returned default that is not loaded yet, preserving existing sessions and drafts. An invalid receipt retains local edits and requires reconciliation by retrying the original deletion. Deletions and default-preference writes are mutually exclusive within one engine.

Writes and uncertainty

Services enforce identities, ownership, permissions and revisions. The engine rechecks UI grants but cannot authenticate a backend request. System view rename/delete remain forbidden. save and rename responses must preserve the instance identity and return the server's configuration/revision.

A logical create retains ViewCreateContext.requestId across uncertain retries. The service must deduplicate that ID and reject reuse with a different body. A UI cancellation or navigation does not establish whether a write committed. Use the authoritative receipt/reload behavior rather than repeating a new create blindly.

ViewServiceError(code, message) distinguishes INVALID_ARGUMENT, UNAUTHENTICATED, FORBIDDEN, NOT_FOUND, CONFLICT, REVISION_CONFLICT, PRECONDITION_REQUIRED, CORRUPT_STATE, UNAVAILABLE and UNKNOWN_OUTCOME. These are service categories, not a public HTTP status mapping.

After save, rename or delete is dispatched, UNKNOWN_OUTCOME, UNAVAILABLE and unclassified exceptions mark requiresReload and block unrelated writes to that instance while preserving local edits. Save and rename require a successful reloadInstance() to obtain the authoritative revision. A missing or inaccessible instance keeps the recovery error and edits. Hosts must report definitive rejections with the corresponding ViewServiceError code. An uncertain create can replay its original request ID; an uncertain delete can replay the same ID and revision. getCapabilitiesSnapshot().instances[id].retryDelete exposes that exception for subscribed UI controls.

Browser persistence and in-process services

Use IndexedDBViewHost from /react for browser persistence and MemoryViewHost from the core entry for memory examples and Node HTTP fixtures. They share permission, revision, create-receipt, user-isolation, ordering and default-view rules; storage uses native IndexedDB transactions or a Map respectively.

Required options: serviceKey, scopeKey, definition, instances, resolveSource. Optional policy callbacks: instancePermissions, canReorder, permissionsRevision. The browser host additionally accepts databaseName (default fve-view-state); the memory host accepts store?: Map<string, string | null>. Memory hosts share state only when explicitly given the same Map.

ts
import { IndexedDBViewHost } from '@ahoo-wang/fetcher-view-engine/react';

const host = new IndexedDBViewHost({
  serviceKey: 'demo-service',
  scopeKey: 'user-a',
  definition,
  instances,
  resolveSource,
});

Browser reads, CAS and writes use one readwrite transaction. Success follows commit; failure and cancellation roll back. reset() atomically clears the service/definition's user views, ordering and receipts. Business records remain a separate query source. In-memory transactions complete read, validation and update in one synchronous JS call stack.

saveDefault stores a preference scoped to the current user and definition. It accepts any currently visible personal, shared or system instance without requiring edit permission, or null; null leaves the next entry without automatic selection. Setting a default does not select it or query records, and later ordering changes do not change it. When a default instance is deleted, the host transaction updates every affected user's default to the first remaining instance in that user's visible order, or null. Explicit null and another user's still-visible personal instance with the same ID remain unchanged. A user first seen after that deletion seeds a default resolved against their actual visibility. Deleting an inaccessible personal instance is still a scoped no-op.

After building, run pnpm verify:view-engine for packed-package, HTTP, cross-tab CAS, cancellation, reset and real-page checks. Client storage and development services are not production authorization boundaries.

Reload does not automatically overwrite a remote content divergence. Inspect session.conflict and explicitly use the remote version or confirm overwrite with the reviewed snapshot. Dispatched write deadlines remain unknown outcomes; read deadlines are independently retryable. See lifecycle and limits.

Released under the Apache License 2.0.