@trycua/cua 0.2.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.
Files changed (91) hide show
  1. package/README.md +121 -0
  2. package/bin/cua.js +37 -0
  3. package/browser/cua_sdk-ffi.d.ts +6 -0
  4. package/browser/cua_sdk-ffi.js +3 -0
  5. package/browser/cua_sdk-ffi.ts +14 -0
  6. package/browser/cua_sdk.d.ts +2237 -0
  7. package/browser/cua_sdk.js +2607 -0
  8. package/browser/cua_sdk.ts +3644 -0
  9. package/browser/cyclops_sdk_schema-ffi.d.ts +6 -0
  10. package/browser/cyclops_sdk_schema-ffi.js +3 -0
  11. package/browser/cyclops_sdk_schema-ffi.ts +14 -0
  12. package/browser/cyclops_sdk_schema.d.ts +996 -0
  13. package/browser/cyclops_sdk_schema.js +2194 -0
  14. package/browser/cyclops_sdk_schema.ts +2934 -0
  15. package/browser/fleet_sdk-ffi.d.ts +28 -0
  16. package/browser/fleet_sdk-ffi.js +3 -0
  17. package/browser/fleet_sdk-ffi.ts +35 -0
  18. package/browser/fleet_sdk.d.ts +3056 -0
  19. package/browser/fleet_sdk.js +5088 -0
  20. package/browser/fleet_sdk.ts +6830 -0
  21. package/browser/index.d.ts +2 -0
  22. package/browser/index.js +18 -0
  23. package/browser/index.web.ts +34 -0
  24. package/browser/tsconfig.json +20 -0
  25. package/browser/wasm-bindgen/index.d.ts +2346 -0
  26. package/browser/wasm-bindgen/index.js +6642 -0
  27. package/browser/wasm-bindgen/index_bg.wasm +0 -0
  28. package/browser/wasm-bindgen/index_bg.wasm.d.ts +1154 -0
  29. package/dist/index.d.ts +118 -0
  30. package/dist/index.js +207 -0
  31. package/dist/mcp.d.ts +33 -0
  32. package/dist/mcp.js +41 -0
  33. package/dist/native/cua_sdk-ffi.d.ts +1038 -0
  34. package/dist/native/cua_sdk-ffi.js +5095 -0
  35. package/dist/native/cua_sdk.d.ts +15906 -0
  36. package/dist/native/cua_sdk.js +27061 -0
  37. package/dist/native/index.d.ts +7 -0
  38. package/dist/native/index.js +12 -0
  39. package/dist/native/node-runtime.d.ts +72 -0
  40. package/dist/native/node-runtime.js +35 -0
  41. package/dist/spaces/cursorArt.d.ts +27 -0
  42. package/dist/spaces/cursorArt.js +59 -0
  43. package/dist/spaces/errors.d.ts +81 -0
  44. package/dist/spaces/errors.js +102 -0
  45. package/dist/spaces/events.d.ts +154 -0
  46. package/dist/spaces/events.js +182 -0
  47. package/dist/spaces/groups.d.ts +130 -0
  48. package/dist/spaces/groups.js +275 -0
  49. package/dist/spaces/host.d.ts +71 -0
  50. package/dist/spaces/host.js +154 -0
  51. package/dist/spaces/index.d.ts +48 -0
  52. package/dist/spaces/index.js +44 -0
  53. package/dist/spaces/pip.d.ts +128 -0
  54. package/dist/spaces/pip.js +250 -0
  55. package/dist/spaces/presence.d.ts +187 -0
  56. package/dist/spaces/presence.js +449 -0
  57. package/dist/spaces/presenceTypes.check.d.ts +12 -0
  58. package/dist/spaces/presenceTypes.check.js +1 -0
  59. package/dist/spaces/presenceTypes.d.ts +47 -0
  60. package/dist/spaces/presenceTypes.js +7 -0
  61. package/dist/spaces/routines.d.ts +158 -0
  62. package/dist/spaces/routines.js +339 -0
  63. package/dist/spaces/thread.d.ts +147 -0
  64. package/dist/spaces/thread.js +285 -0
  65. package/dist/spaces/transport/http.d.ts +51 -0
  66. package/dist/spaces/transport/http.js +113 -0
  67. package/dist/spaces/transport/index.d.ts +19 -0
  68. package/dist/spaces/transport/index.js +19 -0
  69. package/dist/spaces/transport/session.d.ts +120 -0
  70. package/dist/spaces/transport/session.js +188 -0
  71. package/dist/spaces/transport/tauri.d.ts +50 -0
  72. package/dist/spaces/transport/tauri.js +90 -0
  73. package/dist/spaces/transport/types.d.ts +123 -0
  74. package/dist/spaces/transport/types.js +59 -0
  75. package/dist/teleport/controller.d.ts +34 -0
  76. package/dist/teleport/controller.js +107 -0
  77. package/dist/teleport/drop.d.ts +62 -0
  78. package/dist/teleport/drop.js +138 -0
  79. package/dist/teleport/dropZone.d.ts +59 -0
  80. package/dist/teleport/dropZone.js +166 -0
  81. package/dist/teleport/element.d.ts +28 -0
  82. package/dist/teleport/element.js +340 -0
  83. package/dist/teleport/index.d.ts +32 -0
  84. package/dist/teleport/index.js +32 -0
  85. package/dist/teleport/install.d.ts +31 -0
  86. package/dist/teleport/install.js +65 -0
  87. package/dist/teleport/model.d.ts +245 -0
  88. package/dist/teleport/model.js +334 -0
  89. package/dist/teleport/windowDrag.d.ts +86 -0
  90. package/dist/teleport/windowDrag.js +71 -0
  91. package/package.json +93 -0
@@ -0,0 +1,187 @@
1
+ /**
2
+ * `@trycua/cua/spaces/presence`: the shared presence cursor, as data. No
3
+ * native library, no Node imports.
4
+ *
5
+ * A {@link PresenceRoster} folds a presence session's events (`joined`,
6
+ * `left`, `cursor_moved`) into who is here and where their cursor is, so a
7
+ * shell can draw the other participants' cursors over the stream. Drawing is
8
+ * the app's; this is the model. {@link waitForPresence} reads a session until
9
+ * an event matches, bounded by time and by event count.
10
+ *
11
+ * ```ts
12
+ * import { PresenceRoster, waitForPresence } from "@trycua/cua/spaces/presence"
13
+ *
14
+ * const session = await space.joinPresence({ id: "me", displayName: "Operator", agent: false }, 10_000n)
15
+ * const roster = PresenceRoster.from(await session.me(), await session.roster())
16
+ * for (;;) { const e = await session.nextEvent(1_000n); if (e) roster.apply(e); draw(roster.others) }
17
+ * ```
18
+ */
19
+ import type { PresenceEvent, PresenceEventSource, PresenceMember, PresenceParticipant } from "./presenceTypes.js";
20
+ export type { PresenceCursor, PresenceEvent, PresenceEventSource, PresenceMember, PresenceParticipant } from "./presenceTypes.js";
21
+ import { type CursorShapeName } from "./cursorArt.js";
22
+ export { CURSOR_ART, CURSOR_SHAPES, cursorArt, cursorArtSvg, isCursorShape } from "./cursorArt.js";
23
+ export type { CursorArtShape, CursorShapeName } from "./cursorArt.js";
24
+ /** The cursor palette, in cua-spacesd's assignment order. */
25
+ export declare const PRESENCE_PALETTE: readonly string[];
26
+ /**
27
+ * The stable presence color of an agent or other principal: a palette color
28
+ * picked by a hash (FNV-1a, 32-bit, over the UTF-8 id), the same answer as the
29
+ * Rust core's `presence_color` in every binding. Request it when the agent
30
+ * joins presence (`PresenceIdentity.color`) and use it as the background of
31
+ * its avatar, so avatar and cursor come from one source.
32
+ */
33
+ export declare function presenceColor(id: string): string;
34
+ /** Black or white text, whichever has the better WCAG contrast on `background` (`#rrggbb`). */
35
+ export declare function presenceTextColor(background: string): string;
36
+ /** An agent's presence identity, requesting its stable {@link presenceColor}. */
37
+ export declare function agentIdentity(id: string, displayName: string): {
38
+ id: string;
39
+ displayName: string;
40
+ color: string;
41
+ agent: true;
42
+ };
43
+ /** One participant and their last cursor (normalized 0..1 over the streamed surface). */
44
+ export interface PresenceEntry {
45
+ participantId: string;
46
+ /** The identity it joined with (`PresenceIdentity.id`), unless the server asserts one. */
47
+ principalId: string;
48
+ displayName: string;
49
+ color: string;
50
+ agent: boolean;
51
+ cursor?: {
52
+ x: number;
53
+ y: number;
54
+ visible: boolean;
55
+ windowId?: string;
56
+ shape?: CursorShapeName;
57
+ };
58
+ }
59
+ /** Used when the server assigned no color. */
60
+ export declare const PRESENCE_FALLBACK_COLOR = "#3b82f6";
61
+ /** Who is in a Space's presence and where their cursors are. Pure state. */
62
+ export declare class PresenceRoster {
63
+ private readonly byId;
64
+ meId: string | undefined;
65
+ /** A roster seeded from a fresh session: `me` and the members at join. */
66
+ static from(me: PresenceParticipant, members?: readonly PresenceMember[]): PresenceRoster;
67
+ /** Everyone, me first, then in join order. */
68
+ get entries(): PresenceEntry[];
69
+ /**
70
+ * The color an agent shows as, for its avatar and its cursor alike: the
71
+ * color the server assigned once it is present (a requested color is kept
72
+ * unless another participant already holds it), else its stable
73
+ * {@link presenceColor}. Keyed by the id it joined with.
74
+ */
75
+ colorOf(principalId: string): string;
76
+ /** Everyone but me: the cursors to draw (mine is the real pointer). */
77
+ get others(): PresenceEntry[];
78
+ get(participantId: string): PresenceEntry | undefined;
79
+ /** Folds one event in. Returns whether anything changed. */
80
+ apply(e: PresenceEvent): boolean;
81
+ }
82
+ /**
83
+ * Reads `session` until an event matches, folding every event into `roster`
84
+ * when given. Throws after `timeoutMs` or `maxEvents` events.
85
+ */
86
+ export declare function waitForPresence(session: PresenceEventSource, match: (e: PresenceEvent) => boolean, timeoutMs: number, maxEvents?: number, roster?: PresenceRoster): Promise<PresenceEvent>;
87
+ /**
88
+ * The presence netcode model, mirrored exactly from the Rust core
89
+ * (`cua_spaces::presence::view`) and checked against the same conformance
90
+ * vectors. Pure and deterministic: every method takes the local time in
91
+ * milliseconds (`Date.now()`).
92
+ *
93
+ * - your own cursor is drawn at the local pointer, never from the network;
94
+ * - remote cursors render {@link STREAM_DELAY_MS} (100 ms; 66 on the datagram
95
+ * channel) behind the newest sample, Catmull-Rom through at least 4
96
+ * samples, else linear;
97
+ * - past the newest sample they extrapolate for at most 100 ms, then hold;
98
+ * - a jump over 25% of the surface, a target change or a re-show snaps;
99
+ * - a cursor idle for 5 s fades out over 300 ms;
100
+ * - a participant a heartbeat does not list, or everyone when heartbeats
101
+ * stop for 3 intervals, is removed.
102
+ */
103
+ export declare const STREAM_DELAY_MS = 100;
104
+ /** Render delay on the 30 Hz datagram channel. */
105
+ export declare const DATAGRAM_DELAY_MS = 66;
106
+ /** Longest extrapolation past the newest sample. */
107
+ export declare const EXTRAPOLATE_MS = 100;
108
+ /** A jump longer than this (normalized distance) snaps. */
109
+ export declare const SNAP_DISTANCE = 0.25;
110
+ /** A cursor idle this long starts fading. */
111
+ export declare const IDLE_FADE_AFTER_MS = 5000;
112
+ /** Fade duration. */
113
+ export declare const IDLE_FADE_MS = 300;
114
+ /** Heartbeat interval the SDKs ask for. */
115
+ export declare const HEARTBEAT_INTERVAL_MS = 5000;
116
+ /** Missed heartbeats before everyone else is dropped. */
117
+ export declare const HEARTBEAT_MISSES = 3;
118
+ /** Opacity of a cursor idle for `idleMs`. */
119
+ export declare function idleAlpha(idleMs: number): number;
120
+ /** One remote cursor's interpolation buffer. */
121
+ export declare class CursorSmoother {
122
+ readonly delayMs: number;
123
+ private samples;
124
+ private offset;
125
+ private target;
126
+ private lastMoveMs;
127
+ constructor(delayMs?: number);
128
+ /** Adds a sample (`serverMs` 0 = unknown: the local time is used). Out-of-order samples are dropped. */
129
+ push(serverMs: number, localMs: number, x: number, y: number, target: string): void;
130
+ /** Forgets every sample (the next one snaps). */
131
+ reset(): void;
132
+ /** The position to draw at `localMs`, or undefined before any sample. */
133
+ position(localMs: number): [number, number] | undefined;
134
+ /** Opacity at `localMs`. */
135
+ alpha(localMs: number): number;
136
+ }
137
+ /** One cursor to draw now. */
138
+ export interface PresenceDrawable {
139
+ participantId: string;
140
+ displayName: string;
141
+ color: string;
142
+ isMe: boolean;
143
+ isAgent: boolean;
144
+ x: number;
145
+ y: number;
146
+ shape: CursorShapeName;
147
+ shapeSource: string;
148
+ alpha: number;
149
+ displayId: string;
150
+ windowId?: string;
151
+ }
152
+ /** Who is present and what to draw for each, folded from presence events. */
153
+ export declare class PresenceView {
154
+ readonly me: string;
155
+ readonly delayMs: number;
156
+ private readonly members;
157
+ private order;
158
+ private heartbeatIntervalMs;
159
+ private lastHeartbeatMs;
160
+ /** A view for `me` (the caller's participant id). */
161
+ constructor(me: string, delayMs?: number);
162
+ /** A view seeded from a fresh session: `me` and the members at join. */
163
+ static from(me: PresenceParticipant, members?: readonly PresenceMember[], delayMs?: number, localMs?: number): PresenceView;
164
+ /** The expected heartbeat interval (default 5 s). */
165
+ setHeartbeatIntervalMs(ms: number): void;
166
+ /** Adds or updates a participant. */
167
+ upsert(participant: PresenceParticipant): void;
168
+ private remove;
169
+ /** Participant ids, the caller first, then in join order. */
170
+ participantIds(): string[];
171
+ participant(id: string): PresenceParticipant | undefined;
172
+ /** A participant's current shape (the caller's included). */
173
+ shapeOf(id: string): CursorShapeName | undefined;
174
+ /** Folds one event in at `localMs`. Returns whether anything changed. */
175
+ apply(e: PresenceEvent, localMs: number): boolean;
176
+ /** Drops everyone but the caller when heartbeats stopped for 3 intervals. Returns the removed ids. */
177
+ expire(localMs: number): string[];
178
+ /**
179
+ * Everything to draw at `localMs`: remote cursors interpolated and faded,
180
+ * and your own cursor at `pointer` (normalized; undefined when the pointer
181
+ * is not over the surface) with its server shape. Also applies {@link expire}.
182
+ */
183
+ drawables(localMs: number, pointer?: {
184
+ x: number;
185
+ y: number;
186
+ }): PresenceDrawable[];
187
+ }
@@ -0,0 +1,449 @@
1
+ import { isCursorShape } from "./cursorArt.js";
2
+ export { CURSOR_ART, CURSOR_SHAPES, cursorArt, cursorArtSvg, isCursorShape } from "./cursorArt.js";
3
+ /** The cursor palette, in cua-spacesd's assignment order. */
4
+ export const PRESENCE_PALETTE = Object.freeze([
5
+ "#e6194b", "#3cb44b", "#4363d8", "#f58231", "#911eb4", "#46f0f0",
6
+ "#f032e6", "#bcf60c", "#008080", "#9a6324", "#800000", "#000075",
7
+ ]);
8
+ /**
9
+ * The stable presence color of an agent or other principal: a palette color
10
+ * picked by a hash (FNV-1a, 32-bit, over the UTF-8 id), the same answer as the
11
+ * Rust core's `presence_color` in every binding. Request it when the agent
12
+ * joins presence (`PresenceIdentity.color`) and use it as the background of
13
+ * its avatar, so avatar and cursor come from one source.
14
+ */
15
+ export function presenceColor(id) {
16
+ let h = 0x811c9dc5;
17
+ for (const b of new TextEncoder().encode(id)) {
18
+ h ^= b;
19
+ h = Math.imul(h, 0x01000193) >>> 0;
20
+ }
21
+ return PRESENCE_PALETTE[h % PRESENCE_PALETTE.length];
22
+ }
23
+ /** Black or white text, whichever has the better WCAG contrast on `background` (`#rrggbb`). */
24
+ export function presenceTextColor(background) {
25
+ const hex = background.replace(/^#/, "");
26
+ if (!/^[0-9a-fA-F]{6}$/.test(hex))
27
+ return "#000000";
28
+ const v = parseInt(hex, 16);
29
+ const lin = (c) => {
30
+ const x = c / 255;
31
+ return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
32
+ };
33
+ const l = 0.2126 * lin((v >> 16) & 0xff) + 0.7152 * lin((v >> 8) & 0xff) + 0.0722 * lin(v & 0xff);
34
+ return 1.05 / (l + 0.05) >= (l + 0.05) / 0.05 ? "#ffffff" : "#000000";
35
+ }
36
+ /** An agent's presence identity, requesting its stable {@link presenceColor}. */
37
+ export function agentIdentity(id, displayName) {
38
+ return { id, displayName, color: presenceColor(id), agent: true };
39
+ }
40
+ /** Used when the server assigned no color. */
41
+ export const PRESENCE_FALLBACK_COLOR = "#3b82f6";
42
+ function entry(p) {
43
+ return {
44
+ participantId: p.participantId,
45
+ principalId: p.principalId,
46
+ displayName: p.displayName,
47
+ color: p.color || PRESENCE_FALLBACK_COLOR,
48
+ agent: p.kind === "agent",
49
+ };
50
+ }
51
+ /** Who is in a Space's presence and where their cursors are. Pure state. */
52
+ export class PresenceRoster {
53
+ byId = new Map();
54
+ meId;
55
+ /** A roster seeded from a fresh session: `me` and the members at join. */
56
+ static from(me, members = []) {
57
+ const r = new PresenceRoster();
58
+ r.meId = me.participantId;
59
+ r.byId.set(me.participantId, entry(me));
60
+ for (const m of members) {
61
+ const e = entry(m.participant);
62
+ if (m.cursor)
63
+ e.cursor = { x: m.cursor.x, y: m.cursor.y, visible: m.cursor.visible, ...(m.cursor.windowId ? { windowId: m.cursor.windowId } : {}) };
64
+ if (!r.byId.has(e.participantId))
65
+ r.byId.set(e.participantId, e);
66
+ }
67
+ return r;
68
+ }
69
+ /** Everyone, me first, then in join order. */
70
+ get entries() {
71
+ const all = [...this.byId.values()];
72
+ return all.sort((a, b) => Number(b.participantId === this.meId) - Number(a.participantId === this.meId));
73
+ }
74
+ /**
75
+ * The color an agent shows as, for its avatar and its cursor alike: the
76
+ * color the server assigned once it is present (a requested color is kept
77
+ * unless another participant already holds it), else its stable
78
+ * {@link presenceColor}. Keyed by the id it joined with.
79
+ */
80
+ colorOf(principalId) {
81
+ for (const e of this.byId.values())
82
+ if (e.principalId === principalId)
83
+ return e.color;
84
+ return presenceColor(principalId);
85
+ }
86
+ /** Everyone but me: the cursors to draw (mine is the real pointer). */
87
+ get others() {
88
+ return [...this.byId.values()].filter((e) => e.participantId !== this.meId);
89
+ }
90
+ get(participantId) {
91
+ return this.byId.get(participantId);
92
+ }
93
+ /** Folds one event in. Returns whether anything changed. */
94
+ apply(e) {
95
+ switch (e.kind) {
96
+ case "joined": {
97
+ if (!e.participant || this.byId.has(e.participant.participantId))
98
+ return false;
99
+ this.byId.set(e.participant.participantId, entry(e.participant));
100
+ return true;
101
+ }
102
+ case "left":
103
+ return e.participantId !== undefined && this.byId.delete(e.participantId);
104
+ case "cursor_moved": {
105
+ const who = e.participantId !== undefined ? this.byId.get(e.participantId) : undefined;
106
+ if (!who || !e.cursor)
107
+ return false;
108
+ const keep = who.cursor?.shape;
109
+ const shape = e.cursor.shapeSource && e.cursor.shapeSource !== "unspecified" && isCursorShape(e.cursor.shape) ? e.cursor.shape : keep;
110
+ who.cursor = { x: e.cursor.x, y: e.cursor.y, visible: e.cursor.visible, ...(e.cursor.windowId ? { windowId: e.cursor.windowId } : {}), ...(shape ? { shape } : {}) };
111
+ return true;
112
+ }
113
+ case "shape_changed": {
114
+ const who = e.participantId !== undefined ? this.byId.get(e.participantId) : undefined;
115
+ if (!who?.cursor || !isCursorShape(e.shape))
116
+ return false;
117
+ who.cursor.shape = e.shape;
118
+ return true;
119
+ }
120
+ case "heartbeat": {
121
+ const live = new Set(e.participantIds ?? []);
122
+ let changed = false;
123
+ for (const id of [...this.byId.keys()]) {
124
+ if (id !== this.meId && !live.has(id))
125
+ changed = this.byId.delete(id) || changed;
126
+ }
127
+ return changed;
128
+ }
129
+ default:
130
+ return false;
131
+ }
132
+ }
133
+ }
134
+ /**
135
+ * Reads `session` until an event matches, folding every event into `roster`
136
+ * when given. Throws after `timeoutMs` or `maxEvents` events.
137
+ */
138
+ export async function waitForPresence(session, match, timeoutMs, maxEvents = 50, roster) {
139
+ const deadline = Date.now() + timeoutMs;
140
+ for (let i = 0; i < maxEvents; i++) {
141
+ const left = deadline - Date.now();
142
+ if (left <= 0)
143
+ break;
144
+ const e = await session.nextEvent(BigInt(left));
145
+ if (!e)
146
+ break;
147
+ roster?.apply(e);
148
+ if (match(e))
149
+ return e;
150
+ }
151
+ throw new Error(`presence: no matching event within ${timeoutMs} ms / ${maxEvents} events`);
152
+ }
153
+ // ---------------------------------------------------------------- netcode
154
+ /**
155
+ * The presence netcode model, mirrored exactly from the Rust core
156
+ * (`cua_spaces::presence::view`) and checked against the same conformance
157
+ * vectors. Pure and deterministic: every method takes the local time in
158
+ * milliseconds (`Date.now()`).
159
+ *
160
+ * - your own cursor is drawn at the local pointer, never from the network;
161
+ * - remote cursors render {@link STREAM_DELAY_MS} (100 ms; 66 on the datagram
162
+ * channel) behind the newest sample, Catmull-Rom through at least 4
163
+ * samples, else linear;
164
+ * - past the newest sample they extrapolate for at most 100 ms, then hold;
165
+ * - a jump over 25% of the surface, a target change or a re-show snaps;
166
+ * - a cursor idle for 5 s fades out over 300 ms;
167
+ * - a participant a heartbeat does not list, or everyone when heartbeats
168
+ * stop for 3 intervals, is removed.
169
+ */
170
+ export const STREAM_DELAY_MS = 100;
171
+ /** Render delay on the 30 Hz datagram channel. */
172
+ export const DATAGRAM_DELAY_MS = 66;
173
+ /** Longest extrapolation past the newest sample. */
174
+ export const EXTRAPOLATE_MS = 100;
175
+ /** A jump longer than this (normalized distance) snaps. */
176
+ export const SNAP_DISTANCE = 0.25;
177
+ /** A cursor idle this long starts fading. */
178
+ export const IDLE_FADE_AFTER_MS = 5_000;
179
+ /** Fade duration. */
180
+ export const IDLE_FADE_MS = 300;
181
+ /** Heartbeat interval the SDKs ask for. */
182
+ export const HEARTBEAT_INTERVAL_MS = 5_000;
183
+ /** Missed heartbeats before everyone else is dropped. */
184
+ export const HEARTBEAT_MISSES = 3;
185
+ const MAX_SAMPLES = 8;
186
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
187
+ /** Opacity of a cursor idle for `idleMs`. */
188
+ export function idleAlpha(idleMs) {
189
+ if (idleMs <= IDLE_FADE_AFTER_MS)
190
+ return 1;
191
+ return clamp01(1 - (idleMs - IDLE_FADE_AFTER_MS) / IDLE_FADE_MS);
192
+ }
193
+ /** One remote cursor's interpolation buffer. */
194
+ export class CursorSmoother {
195
+ delayMs;
196
+ samples = [];
197
+ offset;
198
+ target = "";
199
+ lastMoveMs = Number.NEGATIVE_INFINITY;
200
+ constructor(delayMs = STREAM_DELAY_MS) {
201
+ this.delayMs = delayMs;
202
+ }
203
+ /** Adds a sample (`serverMs` 0 = unknown: the local time is used). Out-of-order samples are dropped. */
204
+ push(serverMs, localMs, x, y, target) {
205
+ const t = serverMs > 0 ? serverMs : localMs;
206
+ const candidate = localMs - t;
207
+ this.offset = this.offset !== undefined && this.offset <= candidate ? this.offset : candidate;
208
+ x = clamp01(x);
209
+ y = clamp01(y);
210
+ const last = this.samples[this.samples.length - 1];
211
+ if (last) {
212
+ if (t <= last.t)
213
+ return;
214
+ const jump = Math.sqrt((x - last.x) ** 2 + (y - last.y) ** 2);
215
+ if (target !== this.target || jump > SNAP_DISTANCE)
216
+ this.samples = [];
217
+ if (x !== last.x || y !== last.y)
218
+ this.lastMoveMs = localMs;
219
+ }
220
+ else {
221
+ this.lastMoveMs = localMs;
222
+ }
223
+ this.target = target;
224
+ this.samples.push({ t, x, y });
225
+ while (this.samples.length > MAX_SAMPLES)
226
+ this.samples.shift();
227
+ }
228
+ /** Forgets every sample (the next one snaps). */
229
+ reset() {
230
+ this.samples = [];
231
+ }
232
+ /** The position to draw at `localMs`, or undefined before any sample. */
233
+ position(localMs) {
234
+ const s = this.samples;
235
+ const first = s[0];
236
+ const last = s[s.length - 1];
237
+ if (!first || !last)
238
+ return undefined;
239
+ const render = localMs - (this.offset ?? 0) - this.delayMs;
240
+ if (render <= first.t)
241
+ return [first.x, first.y];
242
+ if (render >= last.t) {
243
+ const dt = render - last.t;
244
+ if (dt > EXTRAPOLATE_MS || s.length < 2)
245
+ return [last.x, last.y];
246
+ const prev = s[s.length - 2];
247
+ const span = last.t - prev.t;
248
+ if (span <= 0)
249
+ return [last.x, last.y];
250
+ const vx = (last.x - prev.x) / span;
251
+ const vy = (last.y - prev.y) / span;
252
+ return [clamp01(last.x + vx * dt), clamp01(last.y + vy * dt)];
253
+ }
254
+ let i = 0;
255
+ while (i + 1 < s.length && s[i + 1].t <= render)
256
+ i++;
257
+ const a = s[i];
258
+ const b = s[i + 1];
259
+ const u = (render - a.t) / (b.t - a.t);
260
+ if (s.length < 4)
261
+ return [a.x + (b.x - a.x) * u, a.y + (b.y - a.y) * u];
262
+ const p0 = i > 0 ? s[i - 1] : a;
263
+ const p3 = i + 2 < s.length ? s[i + 2] : b;
264
+ const cr = (q0, q1, q2, q3) => {
265
+ const u2 = u * u;
266
+ const u3 = u2 * u;
267
+ return 0.5 * (2 * q1 + (-q0 + q2) * u + (2 * q0 - 5 * q1 + 4 * q2 - q3) * u2 + (-q0 + 3 * q1 - 3 * q2 + q3) * u3);
268
+ };
269
+ return [clamp01(cr(p0.x, a.x, b.x, p3.x)), clamp01(cr(p0.y, a.y, b.y, p3.y))];
270
+ }
271
+ /** Opacity at `localMs`. */
272
+ alpha(localMs) {
273
+ return idleAlpha(localMs - this.lastMoveMs);
274
+ }
275
+ }
276
+ const targetKey = (c) => (c.windowId ? `w:${c.windowId}` : `d:${c.displayId ?? ""}`);
277
+ /** Who is present and what to draw for each, folded from presence events. */
278
+ export class PresenceView {
279
+ me;
280
+ delayMs;
281
+ members = new Map();
282
+ order = [];
283
+ heartbeatIntervalMs = HEARTBEAT_INTERVAL_MS;
284
+ lastHeartbeatMs;
285
+ /** A view for `me` (the caller's participant id). */
286
+ constructor(me, delayMs = STREAM_DELAY_MS) {
287
+ this.me = me;
288
+ this.delayMs = delayMs;
289
+ }
290
+ /** A view seeded from a fresh session: `me` and the members at join. */
291
+ static from(me, members = [], delayMs = STREAM_DELAY_MS, localMs = Date.now()) {
292
+ const v = new PresenceView(me.participantId, delayMs);
293
+ v.upsert(me);
294
+ for (const m of members) {
295
+ v.upsert(m.participant);
296
+ if (m.cursor)
297
+ v.apply({ kind: "cursor_moved", participantId: m.participant.participantId, cursor: m.cursor }, localMs);
298
+ }
299
+ return v;
300
+ }
301
+ /** The expected heartbeat interval (default 5 s). */
302
+ setHeartbeatIntervalMs(ms) {
303
+ if (ms > 0)
304
+ this.heartbeatIntervalMs = ms;
305
+ }
306
+ /** Adds or updates a participant. */
307
+ upsert(participant) {
308
+ const m = this.members.get(participant.participantId);
309
+ if (m) {
310
+ m.participant = participant;
311
+ return;
312
+ }
313
+ this.order.push(participant.participantId);
314
+ this.members.set(participant.participantId, { participant, smoother: new CursorSmoother(this.delayMs), shape: "arrow", shapeSource: "unspecified" });
315
+ }
316
+ remove(id) {
317
+ if (id === this.me)
318
+ return false;
319
+ this.order = this.order.filter((o) => o !== id);
320
+ return this.members.delete(id);
321
+ }
322
+ /** Participant ids, the caller first, then in join order. */
323
+ participantIds() {
324
+ return [...this.order.filter((id) => id === this.me), ...this.order.filter((id) => id !== this.me)];
325
+ }
326
+ participant(id) {
327
+ return this.members.get(id)?.participant;
328
+ }
329
+ /** A participant's current shape (the caller's included). */
330
+ shapeOf(id) {
331
+ return this.members.get(id)?.shape;
332
+ }
333
+ /** Folds one event in at `localMs`. Returns whether anything changed. */
334
+ apply(e, localMs) {
335
+ switch (e.kind) {
336
+ case "joined": {
337
+ if (!e.participant)
338
+ return false;
339
+ const known = this.members.has(e.participant.participantId);
340
+ this.upsert(e.participant);
341
+ return !known;
342
+ }
343
+ case "left":
344
+ return e.participantId !== undefined && this.remove(e.participantId);
345
+ case "cursor_moved": {
346
+ const m = e.participantId !== undefined ? this.members.get(e.participantId) : undefined;
347
+ const c = e.cursor;
348
+ if (!m || !c)
349
+ return false;
350
+ const received = c.receivedMs && c.receivedMs > 0 ? c.receivedMs : localMs;
351
+ const wasVisible = m.cursor?.visible === true;
352
+ if (c.visible) {
353
+ if (!wasVisible)
354
+ m.smoother.reset();
355
+ m.smoother.push(c.atMs ?? 0, received, c.x, c.y, targetKey(c));
356
+ }
357
+ if (c.shapeSource && c.shapeSource !== "unspecified") {
358
+ m.shape = isCursorShape(c.shape) ? c.shape : "arrow";
359
+ m.shapeSource = c.shapeSource;
360
+ }
361
+ m.cursor = c;
362
+ return true;
363
+ }
364
+ case "shape_changed": {
365
+ const m = e.participantId !== undefined ? this.members.get(e.participantId) : undefined;
366
+ if (!m)
367
+ return false;
368
+ const shape = isCursorShape(e.shape) ? e.shape : "arrow";
369
+ const source = e.shapeSource || "unspecified";
370
+ if (m.shape === shape && m.shapeSource === source)
371
+ return false;
372
+ m.shape = shape;
373
+ m.shapeSource = source;
374
+ return true;
375
+ }
376
+ case "heartbeat": {
377
+ this.lastHeartbeatMs = localMs;
378
+ const live = new Set(e.participantIds ?? []);
379
+ let changed = false;
380
+ for (const id of this.order.filter((o) => o !== this.me && !live.has(o)))
381
+ changed = this.remove(id) || changed;
382
+ return changed;
383
+ }
384
+ default:
385
+ return false;
386
+ }
387
+ }
388
+ /** Drops everyone but the caller when heartbeats stopped for 3 intervals. Returns the removed ids. */
389
+ expire(localMs) {
390
+ if (this.lastHeartbeatMs === undefined)
391
+ return [];
392
+ if (localMs - this.lastHeartbeatMs <= HEARTBEAT_MISSES * this.heartbeatIntervalMs)
393
+ return [];
394
+ const gone = this.order.filter((id) => id !== this.me);
395
+ for (const id of gone)
396
+ this.remove(id);
397
+ return gone;
398
+ }
399
+ /**
400
+ * Everything to draw at `localMs`: remote cursors interpolated and faded,
401
+ * and your own cursor at `pointer` (normalized; undefined when the pointer
402
+ * is not over the surface) with its server shape. Also applies {@link expire}.
403
+ */
404
+ drawables(localMs, pointer) {
405
+ this.expire(localMs);
406
+ const out = [];
407
+ for (const id of this.participantIds()) {
408
+ const m = this.members.get(id);
409
+ if (!m)
410
+ continue;
411
+ const isMe = id === this.me;
412
+ let x;
413
+ let y;
414
+ let alpha = 1;
415
+ if (isMe) {
416
+ if (!pointer)
417
+ continue;
418
+ x = pointer.x;
419
+ y = pointer.y;
420
+ }
421
+ else {
422
+ if (!m.cursor?.visible)
423
+ continue;
424
+ const p = m.smoother.position(localMs);
425
+ if (!p)
426
+ continue;
427
+ alpha = m.smoother.alpha(localMs);
428
+ if (alpha <= 0)
429
+ continue;
430
+ [x, y] = p;
431
+ }
432
+ out.push({
433
+ participantId: id,
434
+ displayName: m.participant.displayName,
435
+ color: m.participant.color || PRESENCE_FALLBACK_COLOR,
436
+ isMe,
437
+ isAgent: m.participant.kind === "agent",
438
+ x,
439
+ y,
440
+ shape: m.shape,
441
+ shapeSource: m.shapeSource,
442
+ alpha,
443
+ displayId: m.cursor?.displayId ?? "",
444
+ ...(m.cursor?.windowId ? { windowId: m.cursor.windowId } : {}),
445
+ });
446
+ }
447
+ return out;
448
+ }
449
+ }
@@ -0,0 +1,12 @@
1
+ import type * as Native from "../native/index.js";
2
+ import type * as Model from "./presenceTypes.js";
3
+ type Holds<T extends true> = T;
4
+ type Assignable<A, B> = [A] extends [B] ? true : false;
5
+ export type PresenceTypesInStep = [
6
+ Holds<Assignable<Native.PresenceParticipant, Model.PresenceParticipant>>,
7
+ Holds<Assignable<Native.PresenceCursor, Model.PresenceCursor>>,
8
+ Holds<Assignable<Native.PresenceEvent, Model.PresenceEvent>>,
9
+ Holds<Assignable<Native.PresenceMember, Model.PresenceMember>>,
10
+ Holds<Assignable<Native.SpacePresenceLike, Model.PresenceEventSource>>
11
+ ];
12
+ export {};
@@ -0,0 +1 @@
1
+ export {};