@synap-core/client 0.1.0 → 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.
package/dist/index.js CHANGED
@@ -1,7 +1,11 @@
1
+ import {
2
+ createRealtimeClient
3
+ } from "./chunk-BVKLN5W3.js";
4
+
1
5
  // src/index.ts
2
6
  import { createTRPCClient, httpBatchLink } from "@trpc/client";
3
7
  function createSynapClient(config) {
4
- return createTRPCClient({
8
+ const trpcClient = createTRPCClient({
5
9
  links: [
6
10
  httpBatchLink({
7
11
  url: `${config.url}/trpc`,
@@ -10,6 +14,14 @@ function createSynapClient(config) {
10
14
  })
11
15
  ]
12
16
  });
17
+ const realtimeClient = config.realtime ? createRealtimeClient({
18
+ url: config.url,
19
+ auth: config.realtime
20
+ }) : null;
21
+ return {
22
+ trpc: trpcClient,
23
+ realtime: realtimeClient
24
+ };
13
25
  }
14
26
  var index_default = createSynapClient;
15
27
  export {
@@ -1,64 +1,10 @@
1
1
  "use client";
2
-
3
- // src/react/provider.tsx
4
- import { createContext, useContext, useMemo } from "react";
5
- import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6
- import { createTRPCReact, httpBatchLink } from "@trpc/react-query";
7
- import { jsx } from "react/jsx-runtime";
8
- var trpc = createTRPCReact();
9
- var SynapContext = createContext(null);
10
- function SynapProvider({ url = "http://localhost:3000", getToken, headers, children }) {
11
- const queryClient = useMemo(() => new QueryClient({
12
- defaultOptions: {
13
- queries: {
14
- staleTime: 5 * 60 * 1e3,
15
- // 5 minutes
16
- retry: 1
17
- }
18
- }
19
- }), []);
20
- const trpcClient = useMemo(() => trpc.createClient({
21
- links: [
22
- httpBatchLink({
23
- url: `${url}/trpc`,
24
- async headers() {
25
- const baseHeaders = {
26
- ...headers
27
- };
28
- if (getToken) {
29
- const token = await getToken();
30
- if (token) {
31
- baseHeaders["Authorization"] = `Bearer ${token}`;
32
- }
33
- }
34
- return baseHeaders;
35
- },
36
- fetch(url2, options) {
37
- return fetch(url2, {
38
- ...options,
39
- credentials: "include"
40
- // Send cookies
41
- });
42
- }
43
- })
44
- ]
45
- }), [url, getToken, headers]);
46
- const contextValue = useMemo(() => ({
47
- api: trpc,
48
- url
49
- }), [url]);
50
- return /* @__PURE__ */ jsx(SynapContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(trpc.Provider, { client: trpcClient, queryClient, children: /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children }) }) });
51
- }
52
- function useSynap() {
53
- const context = useContext(SynapContext);
54
- if (!context) {
55
- throw new Error("useSynap must be used within a SynapProvider");
56
- }
57
- return context;
58
- }
59
- function useSynapClient() {
60
- return useSynap().api;
61
- }
2
+ import {
3
+ SynapProvider,
4
+ trpc,
5
+ useSynap,
6
+ useSynapClient
7
+ } from "../chunk-6MAWMLEW.js";
62
8
  export {
63
9
  SynapProvider,
64
10
  trpc,
@@ -0,0 +1,29 @@
1
+ /**
2
+ * React Hook: usePresence
3
+ *
4
+ * Hook for real-time presence tracking in a view.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * function WhiteboardView({ viewId }: { viewId: string }) {
9
+ * const { client } = useSynapClient();
10
+ * const { users, moveCursor } = usePresence(client?.realtime, viewId);
11
+ *
12
+ * return (
13
+ * <div onMouseMove={(e) => moveCursor(e.clientX, e.clientY)}>
14
+ * {users.map(user => (
15
+ * <Cursor key={user.userId} {...user} />
16
+ * ))}
17
+ * </div>
18
+ * );
19
+ * }
20
+ * ```
21
+ */
22
+ import type { RealtimeClient } from '../realtime.js';
23
+ import type { UserPresence } from '@synap-core/types/realtime';
24
+ export interface UsePresenceReturn {
25
+ users: UserPresence[];
26
+ moveCursor: (x: number, y: number) => void;
27
+ setTyping: (isTyping: boolean) => void;
28
+ }
29
+ export declare function usePresence(client: RealtimeClient | null, viewId: string, viewType?: string): UsePresenceReturn;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * React Hook: useYjs
3
+ *
4
+ * Hook for Yjs CRDT synchronization in a view.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * function TldrawWhiteboard({ roomName }: { roomName: string }) {
9
+ * const { client } = useSynapClient();
10
+ * const { ydoc } = useYjs(client?.realtime, roomName);
11
+ *
12
+ * if (!ydoc) return <div>Connecting...</div>;
13
+ *
14
+ * return <TldrawEditor store={createTLStore({ doc: ydoc })} />;
15
+ * }
16
+ * ```
17
+ */
18
+ import type { RealtimeClient } from '../realtime.js';
19
+ import type { YDoc } from '@synap-core/types/realtime';
20
+ export interface UseYjsReturn {
21
+ ydoc: YDoc | null;
22
+ isConnected: boolean;
23
+ }
24
+ export declare function useYjs(client: RealtimeClient | null, roomName: string): UseYjsReturn;
package/dist/react.cjs CHANGED
@@ -20,19 +20,122 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/react.ts
21
21
  var react_exports = {};
22
22
  __export(react_exports, {
23
- SynapQueryClientProvider: () => import_react_query2.QueryClientProvider,
23
+ SynapQueryClientProvider: () => import_react_query4.QueryClientProvider,
24
24
  createSynapReact: () => createSynapReact,
25
- default: () => react_default
25
+ default: () => react_default,
26
+ useEntities: () => useEntities,
27
+ useEvents: () => useEvents,
28
+ usePresence: () => usePresence,
29
+ useThreads: () => useThreads,
30
+ useYjs: () => useYjs
26
31
  });
27
32
  module.exports = __toCommonJS(react_exports);
28
- var import_react_query = require("@trpc/react-query");
29
- var import_react_query2 = require("@tanstack/react-query");
33
+ var import_react_query3 = require("@trpc/react-query");
34
+
35
+ // src/react/provider.tsx
36
+ var import_react = require("react");
37
+ var import_react_query = require("@tanstack/react-query");
38
+ var import_react_query2 = require("@trpc/react-query");
39
+ var import_jsx_runtime = require("react/jsx-runtime");
40
+ var trpc = (0, import_react_query2.createTRPCReact)();
41
+ var SynapContext = (0, import_react.createContext)(null);
42
+ function useSynap() {
43
+ const context = (0, import_react.useContext)(SynapContext);
44
+ if (!context) {
45
+ throw new Error("useSynap must be used within a SynapProvider");
46
+ }
47
+ return context;
48
+ }
49
+
50
+ // src/react/useEntities.ts
51
+ function useEntities(options = {}) {
52
+ const { api } = useSynap();
53
+ return api.entities.list.useQuery({
54
+ type: options.type,
55
+ limit: options.limit || 50
56
+ });
57
+ }
58
+
59
+ // src/react/useThreads.ts
60
+ function useThreads(_limit = 20) {
61
+ throw new Error("Chat router not yet implemented in API");
62
+ }
63
+
64
+ // src/react/useEvents.ts
65
+ function useEvents(options = {}) {
66
+ const { api } = useSynap();
67
+ return api.events.list.useQuery({
68
+ limit: options.limit || 100,
69
+ type: options.eventTypes ? options.eventTypes[0] : void 0
70
+ });
71
+ }
72
+
73
+ // src/react/usePresence.ts
74
+ var import_react2 = require("react");
75
+ function usePresence(client, viewId, viewType) {
76
+ const [users, setUsers] = (0, import_react2.useState)([]);
77
+ (0, import_react2.useEffect)(() => {
78
+ if (!client) return;
79
+ client.connectPresence(viewId, viewType);
80
+ client.on("presence:init", (data) => {
81
+ setUsers(data.users);
82
+ });
83
+ client.on("user-joined", (user) => {
84
+ setUsers((prev) => [...prev, user]);
85
+ });
86
+ client.on("user-left", (data) => {
87
+ setUsers((prev) => prev.filter((u) => u.userId !== data.userId));
88
+ });
89
+ client.on("cursor-update", (data) => {
90
+ setUsers((prev) => prev.map(
91
+ (u) => u.userId === data.userId ? { ...u, cursor: { x: data.x, y: data.y } } : u
92
+ ));
93
+ });
94
+ return () => {
95
+ client.disconnect();
96
+ };
97
+ }, [client, viewId, viewType]);
98
+ const moveCursor = (0, import_react2.useCallback)((x, y) => {
99
+ client?.moveCursor(x, y);
100
+ }, [client]);
101
+ const setTyping = (0, import_react2.useCallback)((isTyping) => {
102
+ client?.setTyping(isTyping);
103
+ }, [client]);
104
+ return { users, moveCursor, setTyping };
105
+ }
106
+
107
+ // src/react/useYjs.ts
108
+ var import_react3 = require("react");
109
+ function useYjs(client, roomName) {
110
+ const [ydoc, setYdoc] = (0, import_react3.useState)(null);
111
+ const [isConnected, setIsConnected] = (0, import_react3.useState)(false);
112
+ (0, import_react3.useEffect)(() => {
113
+ if (!client) return;
114
+ const doc = client.connectYjs(roomName);
115
+ setYdoc(doc);
116
+ setIsConnected(true);
117
+ return () => {
118
+ client.disconnect();
119
+ setYdoc(null);
120
+ setIsConnected(false);
121
+ };
122
+ }, [client, roomName]);
123
+ return { ydoc, isConnected };
124
+ }
125
+
126
+ // src/react.ts
127
+ var import_react_query4 = require("@tanstack/react-query");
30
128
  function createSynapReact() {
31
- return (0, import_react_query.createTRPCReact)();
129
+ return (0, import_react_query3.createTRPCReact)();
32
130
  }
33
131
  var react_default = createSynapReact;
34
132
  // Annotate the CommonJS export names for ESM import in node:
35
133
  0 && (module.exports = {
36
134
  SynapQueryClientProvider,
37
- createSynapReact
135
+ createSynapReact,
136
+ useEntities,
137
+ useEvents,
138
+ usePresence,
139
+ useThreads,
140
+ useYjs
38
141
  });
package/dist/react.d.ts CHANGED
@@ -71,6 +71,16 @@ export declare function createSynapReact(): ReturnType<typeof createTRPCReact<Ap
71
71
  * Type alias for the Synap tRPC React context
72
72
  */
73
73
  export type SynapReactContext = ReturnType<typeof createSynapReact>;
74
+ /**
75
+ * @synap/client/react - React Integration
76
+ *
77
+ * Re-exports React hooks for use in React applications.
78
+ */
79
+ export { useEntities } from './react/useEntities.js';
80
+ export { useThreads } from './react/useThreads.js';
81
+ export { useEvents } from './react/useEvents.js';
82
+ export { usePresence, type UsePresenceReturn } from './react/usePresence.js';
83
+ export { useYjs, type UseYjsReturn } from './react/useYjs.js';
74
84
  export { QueryClientProvider as SynapQueryClientProvider } from '@tanstack/react-query';
75
85
  export type { AppRouter } from '@synap/api';
76
86
  export default createSynapReact;
package/dist/react.js CHANGED
@@ -1,5 +1,87 @@
1
+ import {
2
+ useSynap
3
+ } from "./chunk-6MAWMLEW.js";
4
+
1
5
  // src/react.ts
2
6
  import { createTRPCReact } from "@trpc/react-query";
7
+
8
+ // src/react/useEntities.ts
9
+ function useEntities(options = {}) {
10
+ const { api } = useSynap();
11
+ return api.entities.list.useQuery({
12
+ type: options.type,
13
+ limit: options.limit || 50
14
+ });
15
+ }
16
+
17
+ // src/react/useThreads.ts
18
+ function useThreads(_limit = 20) {
19
+ throw new Error("Chat router not yet implemented in API");
20
+ }
21
+
22
+ // src/react/useEvents.ts
23
+ function useEvents(options = {}) {
24
+ const { api } = useSynap();
25
+ return api.events.list.useQuery({
26
+ limit: options.limit || 100,
27
+ type: options.eventTypes ? options.eventTypes[0] : void 0
28
+ });
29
+ }
30
+
31
+ // src/react/usePresence.ts
32
+ import { useState, useEffect, useCallback } from "react";
33
+ function usePresence(client, viewId, viewType) {
34
+ const [users, setUsers] = useState([]);
35
+ useEffect(() => {
36
+ if (!client) return;
37
+ client.connectPresence(viewId, viewType);
38
+ client.on("presence:init", (data) => {
39
+ setUsers(data.users);
40
+ });
41
+ client.on("user-joined", (user) => {
42
+ setUsers((prev) => [...prev, user]);
43
+ });
44
+ client.on("user-left", (data) => {
45
+ setUsers((prev) => prev.filter((u) => u.userId !== data.userId));
46
+ });
47
+ client.on("cursor-update", (data) => {
48
+ setUsers((prev) => prev.map(
49
+ (u) => u.userId === data.userId ? { ...u, cursor: { x: data.x, y: data.y } } : u
50
+ ));
51
+ });
52
+ return () => {
53
+ client.disconnect();
54
+ };
55
+ }, [client, viewId, viewType]);
56
+ const moveCursor = useCallback((x, y) => {
57
+ client?.moveCursor(x, y);
58
+ }, [client]);
59
+ const setTyping = useCallback((isTyping) => {
60
+ client?.setTyping(isTyping);
61
+ }, [client]);
62
+ return { users, moveCursor, setTyping };
63
+ }
64
+
65
+ // src/react/useYjs.ts
66
+ import { useState as useState2, useEffect as useEffect2 } from "react";
67
+ function useYjs(client, roomName) {
68
+ const [ydoc, setYdoc] = useState2(null);
69
+ const [isConnected, setIsConnected] = useState2(false);
70
+ useEffect2(() => {
71
+ if (!client) return;
72
+ const doc = client.connectYjs(roomName);
73
+ setYdoc(doc);
74
+ setIsConnected(true);
75
+ return () => {
76
+ client.disconnect();
77
+ setYdoc(null);
78
+ setIsConnected(false);
79
+ };
80
+ }, [client, roomName]);
81
+ return { ydoc, isConnected };
82
+ }
83
+
84
+ // src/react.ts
3
85
  import { QueryClientProvider } from "@tanstack/react-query";
4
86
  function createSynapReact() {
5
87
  return createTRPCReact();
@@ -8,5 +90,10 @@ var react_default = createSynapReact;
8
90
  export {
9
91
  QueryClientProvider as SynapQueryClientProvider,
10
92
  createSynapReact,
11
- react_default as default
93
+ react_default as default,
94
+ useEntities,
95
+ useEvents,
96
+ usePresence,
97
+ useThreads,
98
+ useYjs
12
99
  };
package/dist/realtime.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,103 +17,137 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/realtime.ts
21
31
  var realtime_exports = {};
22
32
  __export(realtime_exports, {
23
- SynapRealtimeClient: () => SynapRealtimeClient
33
+ RealtimeClient: () => RealtimeClient,
34
+ createRealtimeClient: () => createRealtimeClient
24
35
  });
25
36
  module.exports = __toCommonJS(realtime_exports);
26
- var SynapRealtimeClient = class {
27
- ws = null;
28
- config;
29
- reconnectAttempts = 0;
30
- maxReconnectAttempts;
31
- reconnectDelay;
32
- reconnectTimer = null;
33
- isIntentionallyDisconnected = false;
37
+ var import_socket = require("socket.io-client");
38
+ var Y = __toESM(require("yjs"), 1);
39
+ var RealtimeClient = class {
34
40
  constructor(config) {
35
41
  this.config = config;
36
- this.maxReconnectAttempts = config.maxReconnectAttempts ?? 5;
37
- this.reconnectDelay = config.reconnectDelay ?? 1e3;
38
42
  }
43
+ presenceSocket = null;
44
+ yjsSocket = null;
45
+ listeners = /* @__PURE__ */ new Map();
39
46
  /**
40
- * Connect to the WebSocket server
47
+ * Connect to presence namespace for real-time collaboration
41
48
  */
42
- connect() {
43
- if (this.ws?.readyState === WebSocket.OPEN) {
44
- return;
45
- }
46
- this.isIntentionallyDisconnected = false;
47
- const wsUrl = this.config.url;
48
- try {
49
- this.ws = new WebSocket(wsUrl);
50
- this.ws.onopen = () => {
51
- this.reconnectAttempts = 0;
52
- this.config.onConnect?.();
53
- };
54
- this.ws.onmessage = (event) => {
55
- try {
56
- const message = JSON.parse(event.data);
57
- this.config.onMessage?.(message);
58
- } catch (error) {
59
- console.error("Failed to parse WebSocket message:", error);
60
- this.config.onError?.(
61
- new Error(`Failed to parse message: ${error instanceof Error ? error.message : String(error)}`)
62
- );
63
- }
64
- };
65
- this.ws.onerror = () => {
66
- this.config.onError?.(new Error("WebSocket error"));
67
- };
68
- this.ws.onclose = () => {
69
- this.config.onDisconnect?.();
70
- if (!this.isIntentionallyDisconnected) {
71
- this.attemptReconnect();
72
- }
73
- };
74
- } catch (error) {
75
- this.config.onError?.(
76
- error instanceof Error ? error : new Error(String(error))
77
- );
49
+ connectPresence(viewId, viewType = "document") {
50
+ if (this.presenceSocket) {
51
+ this.presenceSocket.disconnect();
78
52
  }
53
+ this.presenceSocket = (0, import_socket.io)(`${this.config.url}/presence`, {
54
+ auth: {
55
+ ...this.config.auth,
56
+ viewId,
57
+ viewType
58
+ }
59
+ });
60
+ this.presenceSocket.on("presence:init", (data) => {
61
+ this.emit("presence:init", data);
62
+ });
63
+ this.presenceSocket.on("user:joined", (user) => {
64
+ this.emit("user-joined", user);
65
+ });
66
+ this.presenceSocket.on("user:left", (data) => {
67
+ this.emit("user-left", data);
68
+ });
69
+ this.presenceSocket.on("cursor:update", (data) => {
70
+ this.emit("cursor-update", data);
71
+ });
72
+ this.presenceSocket.on("typing", (data) => {
73
+ this.emit("typing", data);
74
+ });
79
75
  }
80
76
  /**
81
- * Disconnect from the WebSocket server
77
+ * Connect to Yjs namespace for CRDT synchronization
78
+ *
79
+ * @returns Yjs document that auto-syncs with server
82
80
  */
83
- disconnect() {
84
- this.isIntentionallyDisconnected = true;
85
- if (this.reconnectTimer) {
86
- clearTimeout(this.reconnectTimer);
87
- this.reconnectTimer = null;
81
+ connectYjs(roomName) {
82
+ if (this.yjsSocket) {
83
+ this.yjsSocket.disconnect();
88
84
  }
89
- if (this.ws) {
90
- this.ws.close();
91
- this.ws = null;
85
+ const ydoc = new Y.Doc();
86
+ this.yjsSocket = (0, import_socket.io)(`${this.config.url}/yjs`, {
87
+ query: { room: roomName }
88
+ });
89
+ this.yjsSocket.on("yjs:sync", (message) => {
90
+ Y.applyUpdate(ydoc, Uint8Array.from(message));
91
+ });
92
+ this.yjsSocket.on("yjs:update", (message) => {
93
+ Y.applyUpdate(ydoc, Uint8Array.from(message), this.yjsSocket);
94
+ });
95
+ ydoc.on("update", (update, origin) => {
96
+ if (origin !== this.yjsSocket) {
97
+ this.yjsSocket?.emit("yjs:update", Array.from(update));
98
+ }
99
+ });
100
+ return ydoc;
101
+ }
102
+ /**
103
+ * Subscribe to events
104
+ */
105
+ on(event, callback) {
106
+ if (!this.listeners.has(event)) {
107
+ this.listeners.set(event, /* @__PURE__ */ new Set());
92
108
  }
109
+ this.listeners.get(event).add(callback);
93
110
  }
94
111
  /**
95
- * Check if currently connected
112
+ * Unsubscribe from events
96
113
  */
97
- isConnected() {
98
- return this.ws?.readyState === WebSocket.OPEN;
114
+ off(event, callback) {
115
+ this.listeners.get(event)?.delete(callback);
99
116
  }
100
- attemptReconnect() {
101
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
102
- this.config.onError?.(
103
- new Error(`Max reconnection attempts (${this.maxReconnectAttempts}) reached`)
104
- );
105
- return;
106
- }
107
- this.reconnectAttempts++;
108
- const delay = this.reconnectDelay * this.reconnectAttempts;
109
- this.reconnectTimer = setTimeout(() => {
110
- this.connect();
111
- }, delay);
117
+ /**
118
+ * Emit event to all listeners
119
+ */
120
+ emit(event, data) {
121
+ this.listeners.get(event)?.forEach((cb) => cb(data));
122
+ }
123
+ /**
124
+ * Move cursor (presence)
125
+ */
126
+ moveCursor(x, y) {
127
+ this.presenceSocket?.emit("cursor:move", { x, y });
128
+ }
129
+ /**
130
+ * Set typing indicator
131
+ */
132
+ setTyping(isTyping) {
133
+ this.presenceSocket?.emit("typing", isTyping);
134
+ }
135
+ /**
136
+ * Disconnect all connections
137
+ */
138
+ disconnect() {
139
+ this.presenceSocket?.disconnect();
140
+ this.yjsSocket?.disconnect();
141
+ this.presenceSocket = null;
142
+ this.yjsSocket = null;
143
+ this.listeners.clear();
112
144
  }
113
145
  };
146
+ function createRealtimeClient(config) {
147
+ return new RealtimeClient(config);
148
+ }
114
149
  // Annotate the CommonJS export names for ESM import in node:
115
150
  0 && (module.exports = {
116
- SynapRealtimeClient
151
+ RealtimeClient,
152
+ createRealtimeClient
117
153
  });