@neta-art/cohub 4.8.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,8 @@
2
2
 
3
3
  This guide explains how to use the Cohub SDK **inside a published Work** — the
4
4
  only environment where runtime APIs (`context()`, `auth.request`,
5
- `work.commerce.*`) function. Read this before writing any Work that calls Cohub
6
- capabilities from browser-side JavaScript.
5
+ `work.commerce.*`, `work.realtime.*`) function. Read this before writing any
6
+ Work that calls Cohub capabilities from browser-side JavaScript.
7
7
 
8
8
  It is written to be self-contained: an agent or developer who reads only this
9
9
  file plus the SDK type definitions should be able to build a working Work
@@ -24,6 +24,7 @@ without reverse-engineering source code.
24
24
  - [File reads](#file-reads-spacefiles)
25
25
  - [Account-level data](#account-level-data-spaceslist--userlistsessions--usergetusage)
26
26
  - [Commerce](#commerce-workcommerce)
27
+ - [Realtime rooms](#realtime-rooms-workrealtime)
27
28
  6. [Complete working example](#6-complete-working-example)
28
29
  7. [Common pitfalls checklist](#7-common-pitfalls-checklist)
29
30
  8. [Publishing a Work (API/SDK)](#8-publishing-a-work-apisdk)
@@ -55,18 +56,19 @@ requiring the viewer to paste an API key.
55
56
  └─────────────────────────────────────────┘
56
57
  ```
57
58
 
58
- Three runtime-only APIs form the foundation; everything else is standard SDK:
59
+ Four runtime-only APIs form the foundation; everything else is standard SDK:
59
60
 
60
61
  | API | What it does | Returns |
61
62
  |---|---|---|
62
63
  | `client.context()` | Asks the host for the Work's identity | `{ work, space, viewer?, permissions }` or `null` |
63
64
  | `client.auth.request({ scopes, reason })` | Shows the viewer a consent dialog; on approval, caches a token carrying those scopes | `true` / `false` |
64
65
  | `client.work.commerce.*` | Entitlement checks, credit consumption, purchases | (see Commerce section) |
66
+ | `client.work.realtime.*` | Temporary rooms, events, presence, and membership | (see Realtime rooms section) |
65
67
 
66
- > **Runtime-only constraint.** These three APIs only work inside a **published**
68
+ > **Runtime-only constraint.** These APIs only work inside a **published**
67
69
  > Work. Outside that context (a static asset URL, a local `file://` preview,
68
- > a plain Node script) `context()` returns `null` and `auth.request` / commerce
69
- > calls fail. Always develop against a published Work.
70
+ > a plain Node script) `context()` returns `null` and the other runtime APIs
71
+ > fail. Always develop against a published Work.
70
72
 
71
73
  ---
72
74
 
@@ -220,6 +222,7 @@ result needs `taskrun.view` (a work scope).
220
222
  | Commerce: entitlements | `client.work.commerce.getEntitlements()` | *(runtime only, no scope)* | — |
221
223
  | Commerce: consume credits | `client.work.commerce.consumeCredits()` | *(runtime only, no scope)* | — |
222
224
  | Commerce: purchase | `client.work.commerce.purchase()` | *(runtime only, no scope)* | — |
225
+ | Realtime rooms | `client.work.realtime.createRoom()` / `joinRoom()` | *(runtime only, no scope)* | — |
223
226
 
224
227
  ### Minimal scope sets for common Work types
225
228
 
@@ -268,11 +271,11 @@ Works are typically single HTML files with no bundler. Import the SDK from an
268
271
  ESM CDN:
269
272
 
270
273
  ```js
271
- import { createCohubClient } from "https://esm.sh/@neta-art/cohub@2";
274
+ import { createCohubClient } from "https://esm.sh/@neta-art/cohub@latest";
272
275
  ```
273
276
 
274
- > Pin to a major version (`@2`) or an exact version (`@2.6.0`) to avoid
275
- > breaking changes. Check `npm view @neta-art/cohub version` for the latest.
277
+ `@latest` keeps a Work on the current SDK release. Pin an exact version only
278
+ when a deployment needs reproducible dependency updates.
276
279
 
277
280
  ### Environment detection — critical
278
281
 
@@ -660,12 +663,120 @@ if (checkoutState.orderId) {
660
663
  retries the call after a timeout, pass the same `purchaseAttemptId` to ensure
661
664
  the retry resolves to the original Billing order.
662
665
 
666
+ ### Realtime rooms (`work.realtime`)
667
+
668
+ **Scopes:** none — uses the published Work's runtime identity without an
669
+ additional consent dialog. The CLI and ordinary server auth cannot create or
670
+ join these rooms.
671
+
672
+ ```js
673
+ const room = await client.work.realtime.createRoom({
674
+ code: "TEAM-ALPHA", // optional; generated when omitted
675
+ maxParticipants: 64,
676
+ expiresInSeconds: 2 * 60 * 60,
677
+ });
678
+
679
+ const stopEvents = room.subscribe("shared.state.updated", (event) => {
680
+ console.log(event.sequence, event.data, event.self);
681
+ });
682
+ console.log(room.members); // initial snapshot
683
+ const stopMembers = room.onMembersChanged((members) => {
684
+ console.log(members);
685
+ });
686
+
687
+ await room.setPresence({ status: "active" });
688
+ await room.publish("shared.state.updated", { value: 42 });
689
+
690
+ stopEvents();
691
+ stopMembers();
692
+ await room.leave();
693
+ ```
694
+
695
+ Join an existing room with
696
+ `client.work.realtime.joinRoom({ code: "TEAM-ALPHA" })`. Codes are scoped to
697
+ one Work and are identifiers, not credentials; the runtime session and a
698
+ short-lived admission ticket provide authorization.
699
+
700
+ | Surface | Purpose |
701
+ |---|---|
702
+ | `createRoom()` / `joinRoom()` | Create or enter a code-scoped room |
703
+ | `subscribe()` / `subscribeAll()` | Receive typed or all business events |
704
+ | `publish()` | Send an acknowledged event; accepts an optional correlation-only `clientEventId` |
705
+ | `send()` / `onSendError()` | Send without an ACK for high-rate traffic; observe asynchronous failures |
706
+ | `members` / `onMembersChanged()` | Read the initial member snapshot and later membership or presence changes |
707
+ | `setPresence()` | Replace this participant's transient presence object |
708
+ | `state` / `onStateChange()` | Observe `connecting`, `joined`, `reconnecting`, `expired`, or `closed` |
709
+ | `onOutOfSync()` | Detect sequence jumps in the current live stream |
710
+ | `leave()` | Release membership and SDK listeners |
711
+
712
+ Room events are ordered while connected but are not replayed. A reconnect
713
+ refreshes the member snapshot and advances the sequence cursor, so use
714
+ `onStateChange()` to resync authoritative application state after reconnecting;
715
+ `onOutOfSync()` only reports gaps visible in the current live stream. Payloads
716
+ are transient and are not stored in the Work.
717
+
718
+ | Limit | Value |
719
+ |---|---|
720
+ | Room code | Generated when omitted; custom codes are 3–48 uppercase letters, digits, `_`, or `-`, starting with a letter or digit |
721
+ | Lifetime | 2 hours by default; 60 seconds to 24 hours, absolute from creation |
722
+ | Participants | 16 by default; 2–128 |
723
+ | Active rooms | 512 per Work |
724
+ | Event name | 1–64 ASCII letters, digits, `.`, `_`, `:`, or `-`, starting with a letter or digit; `cohub.*` is reserved |
725
+ | Event payload | 16 KB of JSON |
726
+ | Presence payload | 2 KB of JSON |
727
+ | Publish rate | 2,000 events per second per room |
728
+ | Presence rate | 30 updates per second per connection |
729
+ | Pending mutations | 256 per connection before backpressure errors |
730
+
731
+ `createRoom()` returns HTTP 429 with `ROOM_QUOTA_EXCEEDED` at the active-room
732
+ limit. Activity never extends `expiresAt`; expired rooms release their slot
733
+ automatically.
734
+
735
+ #### High-frequency events
736
+
737
+ `publish()` waits for an ACK, so a loop that awaits each call is capped at
738
+ roughly `1000 / RTT` events per second. Use `send()` for input frames and other
739
+ high-rate traffic:
740
+
741
+ ```js
742
+ room.onSendError((error) => console.warn("dropped frame", error.message));
743
+ room.onStateChange((state) => {
744
+ if (state !== "joined") pauseSimulation();
745
+ });
746
+
747
+ room.send("input.frame", { frame, pad });
748
+ ```
749
+
750
+ `send()` preserves server ordering but drops calls while the room is not joined
751
+ and reports rate, validation, membership, and backpressure failures through
752
+ `onSendError()`. Use `publish()` whenever a specific event must be confirmed.
753
+
754
+ #### Seats and participant identity
755
+
756
+ Every connection is a participant by default, so two tabs appear twice. Each
757
+ member includes an opaque `userKey` that is stable for one room and viewer,
758
+ letting an application group connections without seeing the account ID.
759
+
760
+ Set `seatPerUser: true` when each viewer should occupy at most one seat:
761
+
762
+ ```js
763
+ const room = await client.work.realtime.createRoom({
764
+ maxParticipants: 2,
765
+ seatPerUser: true,
766
+ });
767
+ ```
768
+
769
+ A second tab or reconnect takes over the existing seat instead of consuming a
770
+ new one. The server keeps the participant ID, updates `room.participantId`, and
771
+ closes the superseded connection without emitting a leave event. Without this
772
+ mode, an unclean disconnect can retain its seat lease for up to one minute.
773
+
663
774
  ---
664
775
 
665
776
  ## 6. Complete working example
666
777
 
667
- A no-build HTML Work that tests LLM chat and image generation. This is the
668
- exact pattern that was verified end-to-end. Adapt it to your needs.
778
+ A no-build HTML Work for LLM chat and image generation. Use it as a starting
779
+ point and keep only the capabilities you need.
669
780
 
670
781
  > **Publish this Work with:**
671
782
  > - workScopes: `["space.view", "session.view", "taskrun.view"]`
@@ -724,7 +835,7 @@ exact pattern that was verified end-to-end. Adapt it to your needs.
724
835
  ### `app.js`
725
836
 
726
837
  ```js
727
- import { createCohubClient } from "https://esm.sh/@neta-art/cohub@2";
838
+ import { createCohubClient } from "https://esm.sh/@neta-art/cohub@latest";
728
839
 
729
840
  // --- Environment detection (critical: browsers don't inject ENV) ---
730
841
  const isDevWork =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub",
3
- "version": "4.8.0",
3
+ "version": "5.0.0",
4
4
  "description": "Cohub SDK for spaces, sessions, boards, and realtime agent collaboration.",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,