@collective-intelligence/external-apps-client-sdk-react

中文文档
0.2.21发布于 2026/11/10TypeScriptES Module近 30 天下载 1.1万

React bindings for Collective Intelligence external applications.

原始使用文档
文档目录7 节
  1. Optimistic Writes
  2. Declared entity views
  3. Realtime topics
  4. Effect bridge
  5. Machine tests
  6. Workspace development
  7. Enum value compatibility

@collective-intelligence/external-apps-client-sdk-react

UI-minimal React bindings over the Promise Client SDK. The package has no private application UI dependencies.

CollectiveIntelligenceProvider owns a stable, request-isolated QueryClient; applications do not configure a QueryClient for SDK CRUD. To use one cache for the SDK hooks and the application, pass the application's client as queryClient. Then the defaults of that client apply to the /resources reads, and the application isolates requests on the server. Entity hooks keep retry: false.

Install these required peer dependencies in the application: react, react-dom, @tanstack/react-query, xstate, effect and @collective-intelligence/ui-react. The package depends on @collective-intelligence/external-apps-client-sdk as a normal dependency. The packed JavaScript bundles none of these packages, so the application holds one copy of each. The ./effect entry holds the Effect bridge for XState machines.

Configure the provider's entitySource once, alongside its existing Promise client. EntitySource exposes a stable getSnapshot, a subscribe function, and a typed bind(contract) function returning the common Promise EntityClient. Authenticated snapshots identify the configured logical source, application, user, and organization. Obtain those identities from the authenticated application session; they are cache boundaries, not access grants. Return a new stable snapshot for every session transition, notify subscribers immediately on logout, and bind clients to that snapshot's authority. Backend authorization remains authoritative. CollectiveIntelligenceLoginProvider forwards the same configuration; the entity source must subscribe to the application's CI-derived session lifecycle.

;<CollectiveIntelligenceProvider client={appSdk} entitySource={entitySource}>
  <Orders />
</CollectiveIntelligenceProvider>

// Import these hooks from /resources. Use the portable entity contract.
const rows = useEntityList(orderContract, query)
const record = useEntityRecord(orderContract, { id: selectedId })
const create = useEntityCreate(orderContract)
const update = useEntityUpdate(orderContract, { id: selectedId })
const remove = useEntityDelete(orderContract, { id: selectedId })

await update.submit({ value: draft, expectedRevision: editedRecord.revision })
// Only on an explicit user request to deliver the same action again:
await update.resubmit()

Reads expose data, loading, refreshing, error, authState, effective binding capabilities, and explicit refresh; list reads also expose empty. Equivalent requests share data across views. Cache identity includes source/application/user/organization and resource/version/query, never view namespaces or layout settings.

Writes expose available, record-scoped pending, status, data, error, submitted, submit, and resubmit. Status distinguishes conflict, rejected, and uncertain from ordinary idle/pending/success. Submission clones and freezes the draft and reviewed revision and generates one operation key. A new submit creates a new action; resubmit preserves the previous action exactly. Keep editable drafts in the form and use submitted for the retained action; a successful replay does not overwrite a newer draft. Same-record update/delete calls serialize across hook instances; different records and separate creates remain independent. Queued writes retain their original revisions. Form unmount does not cancel a dispatched write. Source transitions clear protected reads and reject stale queued actions before dispatch. Queries and mutations never automatically retry; there is no persistence, reload recovery, or automatic revision rebasing.

Entity reads and writes run on the Effect Core of the client SDK, through the entity clients that the entity source binds. Their errors stay ResourceResult values: a read that fails gives error, a ResourceQueryError with the resource error; a write that fails in flight is uncertain. Other client SDK calls follow the client SDK error model: a CiResult operation resolves { ok: false, error } with a CiError, and rejects with CiTransportError when the request does not reach CI. useAgentInvocation shows such a rejection as its error state.

The old public TanStack option factories and native-result resource hooks are no longer exported. Use the deliberate entity hooks above. Existing generic slotted Resource* presentation remains separate from common entity CRUD.

Optimistic Writes

Entity writes are Optimistic Writes. The value is in the QueryClient cache, so every view that reads the record changes at once:

  • Before CI answers an update or a delete, the record view and each cached list page show the new value. A deleted record has deletedAt in its record view and leaves the list pages.
  • A create shows a pending row at the start of each cached first list page. Its id is the idempotency key and its value is the create input. CI's record replaces it.
  • To change the cache only when CI answers, pass optimistic: false in the target: useEntityUpdate(contract, { id, optimistic: false }), also for useEntityDelete.
  • Writes to one record run one at a time, in call order. Providers sharing a QueryClient share this ordering; separate QueryClients and record identities remain independent. An entity read of a record waits until the pending write of that record settles, and a list read waits for the pending writes of its resource. Thus a refetch after a realtime change or a Resync does not replace the shown value.
  • On success, the views show CI's record, and the write ends. Then each list view of the resource reads CI once more, because the write can change which records a list holds and in which order. The record view does not read again when it holds CI's record; after a failure, or when another fetch replaced it, it reads CI once more too. The write does not wait for these reads.
  • On a failure or an interruption, a view goes back to its value from before the write, but only when it still shows the write's value. In a list page, only the write's own row changes, so the rows of pending writes to other records stay. A read outside the entity hooks, for example an app fetchQuery on an entity key, does not wait for a write; the view then keeps that newer value. The read after the write shows CI's value in each case.

onWriteFailed on CollectiveIntelligenceProvider or CollectiveIntelligenceLoginProvider gets a WriteFailed for each entity write that CI refuses or that does not reach CI, after the views went back. error is a CiError (CI refused the write) or a CiTransportError (the request did not reach CI, or its answer was lost); operation is create, update or delete; label is the resource id (contract.id). It is not called for an interruption, or when the entity source changed during the write. The provider calls the onWriteFailed of the latest render. . and ./effect export the WriteFailed type. This package shows no toast: ciWriteFailedToast(messages?) of @collective-intelligence/ui-react/ui returns a handler that shows one in the app's Toaster.

import {
  Toaster,
  ciWriteFailedToast
} from "@collective-intelligence/ui-react/ui"

;<CollectiveIntelligenceProvider
  client={appSdk}
  entitySource={entitySource}
  onWriteFailed={ciWriteFailedToast()}
>
  <Orders />
  <Toaster />
</CollectiveIntelligenceProvider>

Declared entity views

Import defineEntity, EntityTable, EntityList, and EntityForm from @collective-intelligence/external-apps-client-sdk-react/resources. Declare presentation once alongside the portable contract:

const orders = defineEntity({
  contract: orderContract,
  config: {
    fields: { number: { label: "Order number" } },
    table: {
      columns: ["number"],
      variants: { compact: { columns: ["number"] } }
    },
    list: { title: "number" },
    forms: {
      create: { fields: ["number"] },
      update: { fields: ["number"] }
    }
  }
})

// Inside the SDK provider and the host framework's NuqsAdapter:
<EntityTable entity={orders} namespace="orders-main" />
<EntityList entity={orders} namespace="orders-sidebar" />
<EntityTable entity={orders} viewState="memory" variant="compact" />
<EntityForm entity={orders} mode="update" record={selectedOrder} />

The declaration contains labels, field/control/renderer keys and layouts, with no JSX, credentials or storage configuration. Field names are inferred from the contract; configured fields must also exist in its portable schema. Views render public ui-react presentation and share authorized query data when their normalized queries match.

URL-backed tables/lists require a stable, distinct namespace. Install the host adapter once; the proven TanStack Start integration uses NuqsAdapter from nuqs/adapters/tanstack-router inside the router root. Nuqs 2.10.1 owns URL serialization and replace-history updates. Defaults are omitted; unrelated keys, other namespaces and the hash are preserved. Invalid owned values fall back to defaults. Search/filter/sort changes reset the page. For custom controls, use useEntityViewState(namespace, { contract, pageSize }); it exposes state, the normalized query, and setters for page, page size, search, filters and sorting. useMemoryEntityViewState and viewState="memory" need no URL adapter. View namespaces never enter data keys.

Forms infer scalar, enum, date, nullable and required settings from portable schemas. Readonly schema fields cannot become writable through configuration and are omitted from submissions. Richer values need a named control in the form's controls registry; missing controls produce explicit feedback. Custom controls receive the field metadata and return JSON wire values. Named display renderers are supplied separately through renderers.

Pending writes disable overlapping saves. Conflict and unknown-outcome states keep the local draft; Resubmit sends the captured original action, including its key, values and revision. Successful resubmission also keeps newer edits. An update form retains the revision its draft began editing when refreshed record props arrive. Remount it intentionally (for example with a React key) after the user chooses to discard the draft and edit a newer server revision. Drafts and operation keys never enter the URL.

Realtime topics

RealtimeProvider takes the options of createRealtimeClient as props. It creates one client, and so one connection, for its tree, and closes it when the tree unmounts. Its effects and the hooks below it hold the client by reference count. Thus a Strict Mode double mount opens one stream, and a server render creates no client. Without a document prop the client follows the page of the browser. The client reads the other props, such as ticketUrl and fetch, once, when the provider creates it; a later change has no effect. To use new values, remount the provider, for example with a React key. useRealtimeTopic(topic, onChanges) subscribes while the component is mounted and unsubscribes on unmount. A new topic object for the same topic keeps the subscription; null subscribes to nothing. The latest onChanges is called without resubscribing. It receives the changes of the topic in batches: one delivery per tab, 250 to 1000 ms after the first pending change, with the latest change of each record.

Pass onReset and refetch every realtime-driven query in it. The client calls it when the server cannot replay a gap and once after a stream opens without a last event id, the first one of each page included. Without onReset the client cannot recover from such a gap, so a change can be lost. The client calls onAccessChanged when the user's access changes. Both stay callbacks of the application; the client calls the ones of the latest render, also when a render adds or removes one. Only a client with onReset resyncs, so a render that adds or removes onReset replaces the client and its stream. The new stream opens without a last event id, so with onReset it resyncs once.

const AppRealtime = ({ children }: { children: ReactNode }) => {
  const queryClient = useQueryClient()
  return (
    <RealtimeProvider
      ticketUrl="/api/realtime/ticket"
      onReset={() => queryClient.invalidateQueries()}
      onAccessChanged={() => queryClient.invalidateQueries()}
    >
      {children}
    </RealtimeProvider>
  )
}

const LeadBoard = () => {
  const queryClient = useQueryClient()
  useRealtimeTopic({ schema: "crm.lead" }, () =>
    // One invalidation per batch, however many leads changed.
    queryClient.invalidateQueries({ queryKey: ["leads"] })
  )
  // …
}

For the entity hooks, useEntityChanges(contract) of ./resources gives the onChanges that refreshes their reads. The record entry of a changed id reads CI again, unless it already holds the revision of the change, such as the record that the tab's own write stored. Every list of the resource reads CI again, because a change can change which records a list holds. Each read waits for the pending writes of its records.

const LeadRealtime = () => {
  useRealtimeTopic({ schema: "crm.lead" }, useEntityChanges(leadContract))
  return null
}

Effect bridge

./effect connects XState machines to Effect and to the application's QueryClient. It has no Effect Atom bindings.

  • WriteFailed is the type of the onWriteFailed event.
  • fromEffect(run) makes actor logic from an Effect. The actor runs the effect on input.runtime, so annotate input with a runtime: EffectRunner<R> that provides every service of the effect. The actor's output is the effect's value; on a failure, onError gets the typed error or the defect. A stop of the actor interrupts the fiber, and the run ends. After the stop, no output, error or interruption of that run gets to the actor, also when React Strict Mode starts the actor of useActorRef again.
  • EffectRuntimeProvider gives its tree a runtime with QueryCache over the nearest QueryClient; useEffectRuntime() returns it. Put it inside CollectiveIntelligenceProvider. The runtime holds no resource, so the provider never disposes it.
  • QueryCache is the one way for machine actors to read and write remote data; machine context holds none. fetch({ queryKey, query, staleTime? }) returns a fresh cached value or runs query once, with no automatic retry, and caches its value. CiError and CiTransportError stay typed errors; another failure is a defect. While a fetch waits, it keeps the key in use, as a mounted view does. When the last view or fetch of the key stops waiting, for example because its actor stops, TanStack Query cancels the fetch and query is interrupted. Thus in React Strict Mode, the actor that useActorRef starts again can run query a second time. A cancel of the key, for example queryClient.cancelQueries({ queryKey }), also interrupts query. A fetch that waits then returns the cached value, or is interrupted when the key has no value. invalidate(queryKey) marks values stale and refetches the ones that views show. cancel(queryKey) cancels the running fetches and interrupts their queries, so they write no value; it keeps the cached values and starts no fetch. An optimistic write calls cancel, then update, runs the write, and calls invalidate when the write settles. setData(queryKey, value) writes a value that views show at once. update(queryKey, (current) => next) replaces each cached value with the result of the function for its current value, without a refetch, for example an optimistic toggle or a changed row; views show the new values at once. Each key under queryKey must hold a value of the type that the function takes. current is undefined for an entry without a value, an undefined result keeps the value, and a key that the cache does not hold stays absent. update does not cancel a running fetch, which can replace the new value when it ends: call cancel first. remove(queryKey) removes the cached entries without a refetch, for example at sign-out; it cancels a running fetch of a removed key, and the next fetch runs its query. A mounted view of a removed key keeps its last result until it renders again. invalidate, cancel, update and remove apply to queryKey and to each key below it (["crm", "lead"] matches ["crm", "lead", "7"], not ["crm", "leads"]); setData writes queryKey only. Views read the same keys with useQuery.
import type { EffectRunner } from "@collective-intelligence/external-apps-client-sdk-react/effect"
import {
  EffectRuntimeProvider,
  QueryCache,
  fromEffect,
  useEffectRuntime
} from "@collective-intelligence/external-apps-client-sdk-react/effect"

const loadLead = fromEffect(
  (input: Readonly<{ id: string; runtime: EffectRunner<QueryCache> }>) =>
    QueryCache.use((cache) =>
      cache.fetch({ queryKey: ["crm", "lead", input.id], query: readLead(input.id) })
    )
)

<CollectiveIntelligenceProvider client={appSdk} queryClient={queryClient}>
  <EffectRuntimeProvider>
    <LeadEditor id={leadId} />
  </EffectRuntimeProvider>
</CollectiveIntelligenceProvider>

const LeadEditor = ({ id }: { id: string }) => {
  const runtime = useEffectRuntime()
  // `leadMachine` invokes `loadLead` with `{ id, runtime }` from its input.
  const lead = useActorRef(leadMachine, { input: { id, runtime } })
  // …
}

Machine tests

Test a machine with makeTestQueryCache(options?), never with a double made with QueryCache.of({ ... }): that double breaks when QueryCache gets a new member. makeTestQueryCache returns:

  • layer: provides the QueryCache of ./effect, the service that EffectRuntimeProvider provides. Give it to ManagedRuntime.make for the runtime of the machine input.
  • queryClient: the real TanStack QueryClient under the cache. Each member works on it as in the app, with the same key prefixes, cancel and remove. Its entries stay until the test ends (gcTime: Infinity).
  • calls: one { member, queryKey } for each member call, in the order that the calls ran. An effect that the machine only builds is no call.

Options, all optional: seed ([queryKey, data] entries that the QueryClient holds first), queryClient (to use your own), and overrides (a Partial of the members: each override replaces its member only, and the call log records it too).

Assert on the entries, so a machine that skips a call fails the test:

import {
  QueryCache,
  makeTestQueryCache
} from "@collective-intelligence/external-apps-client-sdk-react/effect"
import * as ManagedRuntime from "effect/ManagedRuntime"

const cache = makeTestQueryCache({
  seed: [[["ci-entities", "lead", "7"], lead]]
})
const runtime = ManagedRuntime.make(cache.layer)
const actor = createActor(sessionMachine, { input: { runtime } }).start()
actor.send({ type: "session.signOut" })
await waitFor(actor, (snapshot) => snapshot.matches("idle"))

// Sign-out removed the reads of the user.
expect(
  cache.queryClient.getQueryCache().findAll({ queryKey: ["ci-entities"] })
).toEqual([])
expect(cache.calls).toEqual([{ member: "remove", queryKey: ["ci-entities"] }])
// After an invalidate: cache.queryClient.getQueryState(key)?.isInvalidated === true
await runtime.dispose()

makeTestQueryCache adds no dependency, and a production bundle that does not import it does not contain it (sideEffects: false).

Workspace development

See the repository's Effect demo for the same declared Lead views configured against local Gateway, proxied CI, and direct CI bindings.

Each package export declares an opt-in ci-source condition for workspace development. The local TypeScript configuration enables it for the framework-neutral Client SDK, so either package's changes are consumed without an intermediate rebuild. Release builds disable custom conditions; installed npm consumers resolve generated dist JavaScript and declarations. Shared runtime chunks keep provider contexts identical across the root and /resources entrypoints.

Run bun run test:unit, bun run type-check, bun run lint, and bun run test:pack. The pack check executes a real root-provider/resources-hook consumer without caller TanStack setup, and a ./effect consumer that shares the application's QueryClient. It removes its temporary installation on success or failure. The repository root runs bun run lint:effect for the Effect rules.

Enum value compatibility

Repository-owned enum values use lower snake_case. Update gateway manifests, request payloads and saved resource filters together with their consumers. For example, resource filters use starts_with and is_null, lifecycle operation values use soft_delete and hard_delete, and gateway principal kinds use detached_user and service_principal. Public method and capability property names such as softDelete remain unchanged. External HTTP, OAuth, JSON Schema and provider values retain their required spelling at their adapter boundaries.

版本与发布记录

@collective-intelligence/external-apps-client-sdk-react 共发布 65 个版本,首次发布于 2026/10/08;过去 12 个月发布了 65 个版本。

版本发布日期距今
0.2.21latest2026/11/10今天
0.2.202026/11/10今天
0.2.192026/11/10今天
0.2.182026/11/09今天
0.2.172026/11/09今天
0.2.162026/11/091 天前

关于 @collective-intelligence/external-apps-client-sdk-react 的常见问题

@collective-intelligence/external-apps-client-sdk-react 是什么?

React bindings for Collective Intelligence external applications.

如何安装 @collective-intelligence/external-apps-client-sdk-react?

在项目目录运行 npm install @collective-intelligence/external-apps-client-sdk-react,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(>=18.2 <20)、effect(^4.1.0)、xstate(^5.33.3)、react-dom(>=18.2 <20)、@tanstack/react-query(^5.101.4)、@collective-intelligence/ui-react(0.2.2)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@collective-intelligence/external-apps-client-sdk-react 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@collective-intelligence/external-apps-client-sdk-react 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@collective-intelligence/external-apps-client-sdk-react 可以商用吗?

@collective-intelligence/external-apps-client-sdk-react 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@collective-intelligence/external-apps-client-sdk-react 的下载量有多少?

根据 npm 官方下载统计,@collective-intelligence/external-apps-client-sdk-react 在 2026/10/11 – 2026/11/09 这 30 天内被下载 10,657 次,日均约 355 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@collective-intelligence/external-apps-client-sdk-react 还在维护吗?

最新版本 0.2.21 发布于 2026/11/10(今天);至今共发布 65 个版本,其中过去 12 个月发布了 65 个;首次发布于 2026/10/08。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。