@sofereditor/collab 0.1.0 → 0.3.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.cjs CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  bindCollab: () => bindCollab,
24
24
  getPeers: () => getPeers,
25
25
  observePeers: () => observePeers,
26
+ setLocalCursor: () => setLocalCursor,
26
27
  useCollab: () => useCollab
27
28
  });
28
29
  module.exports = __toCommonJS(index_exports);
@@ -70,9 +71,11 @@ function getPeers(awareness) {
70
71
  const peers = [];
71
72
  awareness.getStates().forEach((state, clientId) => {
72
73
  if (clientId === awareness.clientID) return;
74
+ const s = state;
73
75
  peers.push({
74
76
  clientId,
75
- user: state.user
77
+ user: s.user,
78
+ cursor: s.cursor ?? void 0
76
79
  });
77
80
  });
78
81
  return peers;
@@ -83,6 +86,9 @@ function observePeers(awareness, callback) {
83
86
  emit();
84
87
  return () => awareness.off("change", emit);
85
88
  }
89
+ function setLocalCursor(awareness, cursor) {
90
+ awareness.setLocalStateField("cursor", cursor);
91
+ }
86
92
 
87
93
  // src/useCollab.ts
88
94
  var import_react = require("react");
@@ -119,6 +125,7 @@ function useCollab(options) {
119
125
  bindCollab,
120
126
  getPeers,
121
127
  observePeers,
128
+ setLocalCursor,
122
129
  useCollab
123
130
  });
124
131
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/binding.ts","../src/awareness.ts","../src/useCollab.ts"],"sourcesContent":["export {\n bindCollab,\n type BindCollabOptions,\n type CollabBinding,\n type CollabUser,\n type ConnectionStatus,\n} from \"./binding\";\n\nexport { getPeers, observePeers, type PeerState } from \"./awareness\";\n\nexport {\n useCollab,\n type UseCollabOptions,\n type UseCollabResult,\n} from \"./useCollab\";\n","import type * as Y from \"yjs\";\nimport { HocuspocusProvider } from \"@hocuspocus/provider\";\nimport type { Awareness } from \"y-protocols/awareness\";\n\nexport type ConnectionStatus = \"connecting\" | \"connected\" | \"disconnected\";\n\nexport interface CollabUser {\n name: string;\n color: string;\n}\n\nexport interface BindCollabOptions {\n ydoc: Y.Doc;\n url: string;\n name: string;\n token?: string;\n user?: CollabUser;\n onStatus?: (status: ConnectionStatus) => void;\n onSynced?: (synced: boolean) => void;\n}\n\nexport interface CollabBinding {\n readonly provider: HocuspocusProvider;\n readonly awareness: Awareness;\n setUser(user: CollabUser | null): void;\n destroy(): void;\n}\n\nexport function bindCollab(opts: BindCollabOptions): CollabBinding {\n const provider = new HocuspocusProvider({\n url: opts.url,\n name: opts.name,\n document: opts.ydoc,\n token: opts.token,\n onStatus: opts.onStatus\n ? ({ status }) => opts.onStatus!(status as ConnectionStatus)\n : undefined,\n onSynced: opts.onSynced\n ? ({ state }) => opts.onSynced!(state)\n : undefined,\n });\n\n if (opts.user) {\n provider.awareness?.setLocalStateField(\"user\", opts.user);\n }\n\n let destroyed = false;\n\n return {\n provider,\n get awareness() {\n const a = provider.awareness;\n if (!a) {\n throw new Error(\n \"[@sofereditor/collab] Awareness not yet available (provider not connected).\",\n );\n }\n return a;\n },\n setUser(user) {\n if (destroyed) return;\n provider.awareness?.setLocalStateField(\"user\", user);\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n provider.destroy();\n },\n };\n}\n","import type { Awareness } from \"y-protocols/awareness\";\nimport type { CollabUser } from \"./binding\";\n\nexport interface PeerState {\n clientId: number;\n user?: CollabUser;\n}\n\nexport function getPeers(awareness: Awareness): PeerState[] {\n const peers: PeerState[] = [];\n awareness.getStates().forEach((state, clientId) => {\n if (clientId === awareness.clientID) return;\n peers.push({\n clientId,\n user: (state as { user?: CollabUser }).user,\n });\n });\n return peers;\n}\n\nexport function observePeers(\n awareness: Awareness,\n callback: (peers: PeerState[]) => void,\n): () => void {\n const emit = () => callback(getPeers(awareness));\n awareness.on(\"change\", emit);\n emit();\n return () => awareness.off(\"change\", emit);\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport type * as Y from \"yjs\";\nimport {\n bindCollab,\n type BindCollabOptions,\n type CollabBinding,\n type ConnectionStatus,\n} from \"./binding\";\n\nexport interface UseCollabOptions extends Omit<BindCollabOptions, \"ydoc\"> {\n ydoc: Y.Doc;\n}\n\nexport interface UseCollabResult {\n status: ConnectionStatus;\n synced: boolean;\n binding: CollabBinding | null;\n}\n\nexport function useCollab(options: UseCollabOptions): UseCollabResult {\n const { ydoc, url, name, token, user } = options;\n const [status, setStatus] = useState<ConnectionStatus>(\"connecting\");\n const [synced, setSynced] = useState(false);\n const bindingRef = useRef<CollabBinding | null>(null);\n\n useEffect(() => {\n const binding = bindCollab({\n ydoc,\n url,\n name,\n token,\n user,\n onStatus: (s) => setStatus(s),\n onSynced: (s) => setSynced(s),\n });\n bindingRef.current = binding;\n return () => {\n binding.destroy();\n bindingRef.current = null;\n setStatus(\"disconnected\");\n setSynced(false);\n };\n // Re-bind when document identity or connection params change. `user` is\n // updated via setUser below so changes don't tear down the socket.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [ydoc, url, name, token]);\n\n useEffect(() => {\n bindingRef.current?.setUser(user ?? null);\n }, [user?.name, user?.color]);\n\n return { status, synced, binding: bindingRef.current };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACCA,sBAAmC;AA2B5B,SAAS,WAAW,MAAwC;AACjE,QAAM,WAAW,IAAI,mCAAmB;AAAA,IACtC,KAAK,KAAK;AAAA,IACV,MAAM,KAAK;AAAA,IACX,UAAU,KAAK;AAAA,IACf,OAAO,KAAK;AAAA,IACZ,UAAU,KAAK,WACX,CAAC,EAAE,OAAO,MAAM,KAAK,SAAU,MAA0B,IACzD;AAAA,IACJ,UAAU,KAAK,WACX,CAAC,EAAE,MAAM,MAAM,KAAK,SAAU,KAAK,IACnC;AAAA,EACN,CAAC;AAED,MAAI,KAAK,MAAM;AACb,aAAS,WAAW,mBAAmB,QAAQ,KAAK,IAAI;AAAA,EAC1D;AAEA,MAAI,YAAY;AAEhB,SAAO;AAAA,IACL;AAAA,IACA,IAAI,YAAY;AACd,YAAM,IAAI,SAAS;AACnB,UAAI,CAAC,GAAG;AACN,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,IACA,QAAQ,MAAM;AACZ,UAAI,UAAW;AACf,eAAS,WAAW,mBAAmB,QAAQ,IAAI;AAAA,IACrD;AAAA,IACA,UAAU;AACR,UAAI,UAAW;AACf,kBAAY;AACZ,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF;AACF;;;AC7DO,SAAS,SAAS,WAAmC;AAC1D,QAAM,QAAqB,CAAC;AAC5B,YAAU,UAAU,EAAE,QAAQ,CAAC,OAAO,aAAa;AACjD,QAAI,aAAa,UAAU,SAAU;AACrC,UAAM,KAAK;AAAA,MACT;AAAA,MACA,MAAO,MAAgC;AAAA,IACzC,CAAC;AAAA,EACH,CAAC;AACD,SAAO;AACT;AAEO,SAAS,aACd,WACA,UACY;AACZ,QAAM,OAAO,MAAM,SAAS,SAAS,SAAS,CAAC;AAC/C,YAAU,GAAG,UAAU,IAAI;AAC3B,OAAK;AACL,SAAO,MAAM,UAAU,IAAI,UAAU,IAAI;AAC3C;;;AC5BA,mBAA4C;AAmBrC,SAAS,UAAU,SAA4C;AACpE,QAAM,EAAE,MAAM,KAAK,MAAM,OAAO,KAAK,IAAI;AACzC,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAA2B,YAAY;AACnE,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAAS,KAAK;AAC1C,QAAM,iBAAa,qBAA6B,IAAI;AAEpD,8BAAU,MAAM;AACd,UAAM,UAAU,WAAW;AAAA,MACzB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,CAAC,MAAM,UAAU,CAAC;AAAA,MAC5B,UAAU,CAAC,MAAM,UAAU,CAAC;AAAA,IAC9B,CAAC;AACD,eAAW,UAAU;AACrB,WAAO,MAAM;AACX,cAAQ,QAAQ;AAChB,iBAAW,UAAU;AACrB,gBAAU,cAAc;AACxB,gBAAU,KAAK;AAAA,IACjB;AAAA,EAIF,GAAG,CAAC,MAAM,KAAK,MAAM,KAAK,CAAC;AAE3B,8BAAU,MAAM;AACd,eAAW,SAAS,QAAQ,QAAQ,IAAI;AAAA,EAC1C,GAAG,CAAC,MAAM,MAAM,MAAM,KAAK,CAAC;AAE5B,SAAO,EAAE,QAAQ,QAAQ,SAAS,WAAW,QAAQ;AACvD;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/binding.ts","../src/awareness.ts","../src/useCollab.ts"],"sourcesContent":["export {\n bindCollab,\n type BindCollabOptions,\n type CollabBinding,\n type CollabUser,\n type ConnectionStatus,\n} from \"./binding\";\n\nexport {\n getPeers,\n observePeers,\n setLocalCursor,\n type PeerState,\n} from \"./awareness\";\n\nexport {\n useCollab,\n type UseCollabOptions,\n type UseCollabResult,\n} from \"./useCollab\";\n","import type * as Y from \"yjs\";\nimport { HocuspocusProvider } from \"@hocuspocus/provider\";\nimport type { Awareness } from \"y-protocols/awareness\";\n\nexport type ConnectionStatus = \"connecting\" | \"connected\" | \"disconnected\";\n\nexport interface CollabUser {\n name: string;\n color: string;\n}\n\nexport interface BindCollabOptions {\n ydoc: Y.Doc;\n url: string;\n name: string;\n token?: string;\n user?: CollabUser;\n onStatus?: (status: ConnectionStatus) => void;\n onSynced?: (synced: boolean) => void;\n}\n\nexport interface CollabBinding {\n readonly provider: HocuspocusProvider;\n readonly awareness: Awareness;\n setUser(user: CollabUser | null): void;\n destroy(): void;\n}\n\nexport function bindCollab(opts: BindCollabOptions): CollabBinding {\n const provider = new HocuspocusProvider({\n url: opts.url,\n name: opts.name,\n document: opts.ydoc,\n token: opts.token,\n onStatus: opts.onStatus\n ? ({ status }) => opts.onStatus!(status as ConnectionStatus)\n : undefined,\n onSynced: opts.onSynced\n ? ({ state }) => opts.onSynced!(state)\n : undefined,\n });\n\n if (opts.user) {\n provider.awareness?.setLocalStateField(\"user\", opts.user);\n }\n\n let destroyed = false;\n\n return {\n provider,\n get awareness() {\n const a = provider.awareness;\n if (!a) {\n throw new Error(\n \"[@sofereditor/collab] Awareness not yet available (provider not connected).\",\n );\n }\n return a;\n },\n setUser(user) {\n if (destroyed) return;\n provider.awareness?.setLocalStateField(\"user\", user);\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n provider.destroy();\n },\n };\n}\n","import type { Awareness } from \"y-protocols/awareness\";\nimport type { EncodedCursor } from \"@sofereditor/core\";\nimport type { CollabUser } from \"./binding\";\n\nexport interface PeerState {\n clientId: number;\n user?: CollabUser;\n /** Cursor/seleção remota codificada (Y.RelativePosition + blockId). */\n cursor?: EncodedCursor;\n}\n\nexport function getPeers(awareness: Awareness): PeerState[] {\n const peers: PeerState[] = [];\n awareness.getStates().forEach((state, clientId) => {\n if (clientId === awareness.clientID) return;\n const s = state as { user?: CollabUser; cursor?: EncodedCursor };\n peers.push({\n clientId,\n user: s.user,\n cursor: s.cursor ?? undefined,\n });\n });\n return peers;\n}\n\nexport function observePeers(\n awareness: Awareness,\n callback: (peers: PeerState[]) => void,\n): () => void {\n const emit = () => callback(getPeers(awareness));\n awareness.on(\"change\", emit);\n emit();\n return () => awareness.off(\"change\", emit);\n}\n\n/**\n * Publica (ou limpa, com null) o cursor/seleção local no awareness. O editor\n * chama isto a cada mudança de seleção (com throttle). Os peers leem via\n * `getPeers`/`observePeers`.\n */\nexport function setLocalCursor(\n awareness: Awareness,\n cursor: EncodedCursor | null,\n): void {\n awareness.setLocalStateField(\"cursor\", cursor);\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport type * as Y from \"yjs\";\nimport {\n bindCollab,\n type BindCollabOptions,\n type CollabBinding,\n type ConnectionStatus,\n} from \"./binding\";\n\nexport interface UseCollabOptions extends Omit<BindCollabOptions, \"ydoc\"> {\n ydoc: Y.Doc;\n}\n\nexport interface UseCollabResult {\n status: ConnectionStatus;\n synced: boolean;\n binding: CollabBinding | null;\n}\n\nexport function useCollab(options: UseCollabOptions): UseCollabResult {\n const { ydoc, url, name, token, user } = options;\n const [status, setStatus] = useState<ConnectionStatus>(\"connecting\");\n const [synced, setSynced] = useState(false);\n const bindingRef = useRef<CollabBinding | null>(null);\n\n useEffect(() => {\n const binding = bindCollab({\n ydoc,\n url,\n name,\n token,\n user,\n onStatus: (s) => setStatus(s),\n onSynced: (s) => setSynced(s),\n });\n bindingRef.current = binding;\n return () => {\n binding.destroy();\n bindingRef.current = null;\n setStatus(\"disconnected\");\n setSynced(false);\n };\n // Re-bind when document identity or connection params change. `user` is\n // updated via setUser below so changes don't tear down the socket.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [ydoc, url, name, token]);\n\n useEffect(() => {\n bindingRef.current?.setUser(user ?? null);\n }, [user?.name, user?.color]);\n\n return { status, synced, binding: bindingRef.current };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACCA,sBAAmC;AA2B5B,SAAS,WAAW,MAAwC;AACjE,QAAM,WAAW,IAAI,mCAAmB;AAAA,IACtC,KAAK,KAAK;AAAA,IACV,MAAM,KAAK;AAAA,IACX,UAAU,KAAK;AAAA,IACf,OAAO,KAAK;AAAA,IACZ,UAAU,KAAK,WACX,CAAC,EAAE,OAAO,MAAM,KAAK,SAAU,MAA0B,IACzD;AAAA,IACJ,UAAU,KAAK,WACX,CAAC,EAAE,MAAM,MAAM,KAAK,SAAU,KAAK,IACnC;AAAA,EACN,CAAC;AAED,MAAI,KAAK,MAAM;AACb,aAAS,WAAW,mBAAmB,QAAQ,KAAK,IAAI;AAAA,EAC1D;AAEA,MAAI,YAAY;AAEhB,SAAO;AAAA,IACL;AAAA,IACA,IAAI,YAAY;AACd,YAAM,IAAI,SAAS;AACnB,UAAI,CAAC,GAAG;AACN,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,IACA,QAAQ,MAAM;AACZ,UAAI,UAAW;AACf,eAAS,WAAW,mBAAmB,QAAQ,IAAI;AAAA,IACrD;AAAA,IACA,UAAU;AACR,UAAI,UAAW;AACf,kBAAY;AACZ,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF;AACF;;;AC1DO,SAAS,SAAS,WAAmC;AAC1D,QAAM,QAAqB,CAAC;AAC5B,YAAU,UAAU,EAAE,QAAQ,CAAC,OAAO,aAAa;AACjD,QAAI,aAAa,UAAU,SAAU;AACrC,UAAM,IAAI;AACV,UAAM,KAAK;AAAA,MACT;AAAA,MACA,MAAM,EAAE;AAAA,MACR,QAAQ,EAAE,UAAU;AAAA,IACtB,CAAC;AAAA,EACH,CAAC;AACD,SAAO;AACT;AAEO,SAAS,aACd,WACA,UACY;AACZ,QAAM,OAAO,MAAM,SAAS,SAAS,SAAS,CAAC;AAC/C,YAAU,GAAG,UAAU,IAAI;AAC3B,OAAK;AACL,SAAO,MAAM,UAAU,IAAI,UAAU,IAAI;AAC3C;AAOO,SAAS,eACd,WACA,QACM;AACN,YAAU,mBAAmB,UAAU,MAAM;AAC/C;;;AC7CA,mBAA4C;AAmBrC,SAAS,UAAU,SAA4C;AACpE,QAAM,EAAE,MAAM,KAAK,MAAM,OAAO,KAAK,IAAI;AACzC,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAA2B,YAAY;AACnE,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAAS,KAAK;AAC1C,QAAM,iBAAa,qBAA6B,IAAI;AAEpD,8BAAU,MAAM;AACd,UAAM,UAAU,WAAW;AAAA,MACzB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,CAAC,MAAM,UAAU,CAAC;AAAA,MAC5B,UAAU,CAAC,MAAM,UAAU,CAAC;AAAA,IAC9B,CAAC;AACD,eAAW,UAAU;AACrB,WAAO,MAAM;AACX,cAAQ,QAAQ;AAChB,iBAAW,UAAU;AACrB,gBAAU,cAAc;AACxB,gBAAU,KAAK;AAAA,IACjB;AAAA,EAIF,GAAG,CAAC,MAAM,KAAK,MAAM,KAAK,CAAC;AAE3B,8BAAU,MAAM;AACd,eAAW,SAAS,QAAQ,QAAQ,IAAI;AAAA,EAC1C,GAAG,CAAC,MAAM,MAAM,MAAM,KAAK,CAAC;AAE5B,SAAO,EAAE,QAAQ,QAAQ,SAAS,WAAW,QAAQ;AACvD;","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as Y from 'yjs';
2
2
  import { HocuspocusProvider } from '@hocuspocus/provider';
3
3
  import { Awareness } from 'y-protocols/awareness';
4
+ import { EncodedCursor } from '@sofereditor/core';
4
5
 
5
6
  type ConnectionStatus = "connecting" | "connected" | "disconnected";
6
7
  interface CollabUser {
@@ -27,9 +28,17 @@ declare function bindCollab(opts: BindCollabOptions): CollabBinding;
27
28
  interface PeerState {
28
29
  clientId: number;
29
30
  user?: CollabUser;
31
+ /** Cursor/seleção remota codificada (Y.RelativePosition + blockId). */
32
+ cursor?: EncodedCursor;
30
33
  }
31
34
  declare function getPeers(awareness: Awareness): PeerState[];
32
35
  declare function observePeers(awareness: Awareness, callback: (peers: PeerState[]) => void): () => void;
36
+ /**
37
+ * Publica (ou limpa, com null) o cursor/seleção local no awareness. O editor
38
+ * chama isto a cada mudança de seleção (com throttle). Os peers leem via
39
+ * `getPeers`/`observePeers`.
40
+ */
41
+ declare function setLocalCursor(awareness: Awareness, cursor: EncodedCursor | null): void;
33
42
 
34
43
  interface UseCollabOptions extends Omit<BindCollabOptions, "ydoc"> {
35
44
  ydoc: Y.Doc;
@@ -41,4 +50,4 @@ interface UseCollabResult {
41
50
  }
42
51
  declare function useCollab(options: UseCollabOptions): UseCollabResult;
43
52
 
44
- export { type BindCollabOptions, type CollabBinding, type CollabUser, type ConnectionStatus, type PeerState, type UseCollabOptions, type UseCollabResult, bindCollab, getPeers, observePeers, useCollab };
53
+ export { type BindCollabOptions, type CollabBinding, type CollabUser, type ConnectionStatus, type PeerState, type UseCollabOptions, type UseCollabResult, bindCollab, getPeers, observePeers, setLocalCursor, useCollab };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as Y from 'yjs';
2
2
  import { HocuspocusProvider } from '@hocuspocus/provider';
3
3
  import { Awareness } from 'y-protocols/awareness';
4
+ import { EncodedCursor } from '@sofereditor/core';
4
5
 
5
6
  type ConnectionStatus = "connecting" | "connected" | "disconnected";
6
7
  interface CollabUser {
@@ -27,9 +28,17 @@ declare function bindCollab(opts: BindCollabOptions): CollabBinding;
27
28
  interface PeerState {
28
29
  clientId: number;
29
30
  user?: CollabUser;
31
+ /** Cursor/seleção remota codificada (Y.RelativePosition + blockId). */
32
+ cursor?: EncodedCursor;
30
33
  }
31
34
  declare function getPeers(awareness: Awareness): PeerState[];
32
35
  declare function observePeers(awareness: Awareness, callback: (peers: PeerState[]) => void): () => void;
36
+ /**
37
+ * Publica (ou limpa, com null) o cursor/seleção local no awareness. O editor
38
+ * chama isto a cada mudança de seleção (com throttle). Os peers leem via
39
+ * `getPeers`/`observePeers`.
40
+ */
41
+ declare function setLocalCursor(awareness: Awareness, cursor: EncodedCursor | null): void;
33
42
 
34
43
  interface UseCollabOptions extends Omit<BindCollabOptions, "ydoc"> {
35
44
  ydoc: Y.Doc;
@@ -41,4 +50,4 @@ interface UseCollabResult {
41
50
  }
42
51
  declare function useCollab(options: UseCollabOptions): UseCollabResult;
43
52
 
44
- export { type BindCollabOptions, type CollabBinding, type CollabUser, type ConnectionStatus, type PeerState, type UseCollabOptions, type UseCollabResult, bindCollab, getPeers, observePeers, useCollab };
53
+ export { type BindCollabOptions, type CollabBinding, type CollabUser, type ConnectionStatus, type PeerState, type UseCollabOptions, type UseCollabResult, bindCollab, getPeers, observePeers, setLocalCursor, useCollab };
package/dist/index.js CHANGED
@@ -41,9 +41,11 @@ function getPeers(awareness) {
41
41
  const peers = [];
42
42
  awareness.getStates().forEach((state, clientId) => {
43
43
  if (clientId === awareness.clientID) return;
44
+ const s = state;
44
45
  peers.push({
45
46
  clientId,
46
- user: state.user
47
+ user: s.user,
48
+ cursor: s.cursor ?? void 0
47
49
  });
48
50
  });
49
51
  return peers;
@@ -54,6 +56,9 @@ function observePeers(awareness, callback) {
54
56
  emit();
55
57
  return () => awareness.off("change", emit);
56
58
  }
59
+ function setLocalCursor(awareness, cursor) {
60
+ awareness.setLocalStateField("cursor", cursor);
61
+ }
57
62
 
58
63
  // src/useCollab.ts
59
64
  import { useEffect, useRef, useState } from "react";
@@ -89,6 +94,7 @@ export {
89
94
  bindCollab,
90
95
  getPeers,
91
96
  observePeers,
97
+ setLocalCursor,
92
98
  useCollab
93
99
  };
94
100
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/binding.ts","../src/awareness.ts","../src/useCollab.ts"],"sourcesContent":["import type * as Y from \"yjs\";\nimport { HocuspocusProvider } from \"@hocuspocus/provider\";\nimport type { Awareness } from \"y-protocols/awareness\";\n\nexport type ConnectionStatus = \"connecting\" | \"connected\" | \"disconnected\";\n\nexport interface CollabUser {\n name: string;\n color: string;\n}\n\nexport interface BindCollabOptions {\n ydoc: Y.Doc;\n url: string;\n name: string;\n token?: string;\n user?: CollabUser;\n onStatus?: (status: ConnectionStatus) => void;\n onSynced?: (synced: boolean) => void;\n}\n\nexport interface CollabBinding {\n readonly provider: HocuspocusProvider;\n readonly awareness: Awareness;\n setUser(user: CollabUser | null): void;\n destroy(): void;\n}\n\nexport function bindCollab(opts: BindCollabOptions): CollabBinding {\n const provider = new HocuspocusProvider({\n url: opts.url,\n name: opts.name,\n document: opts.ydoc,\n token: opts.token,\n onStatus: opts.onStatus\n ? ({ status }) => opts.onStatus!(status as ConnectionStatus)\n : undefined,\n onSynced: opts.onSynced\n ? ({ state }) => opts.onSynced!(state)\n : undefined,\n });\n\n if (opts.user) {\n provider.awareness?.setLocalStateField(\"user\", opts.user);\n }\n\n let destroyed = false;\n\n return {\n provider,\n get awareness() {\n const a = provider.awareness;\n if (!a) {\n throw new Error(\n \"[@sofereditor/collab] Awareness not yet available (provider not connected).\",\n );\n }\n return a;\n },\n setUser(user) {\n if (destroyed) return;\n provider.awareness?.setLocalStateField(\"user\", user);\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n provider.destroy();\n },\n };\n}\n","import type { Awareness } from \"y-protocols/awareness\";\nimport type { CollabUser } from \"./binding\";\n\nexport interface PeerState {\n clientId: number;\n user?: CollabUser;\n}\n\nexport function getPeers(awareness: Awareness): PeerState[] {\n const peers: PeerState[] = [];\n awareness.getStates().forEach((state, clientId) => {\n if (clientId === awareness.clientID) return;\n peers.push({\n clientId,\n user: (state as { user?: CollabUser }).user,\n });\n });\n return peers;\n}\n\nexport function observePeers(\n awareness: Awareness,\n callback: (peers: PeerState[]) => void,\n): () => void {\n const emit = () => callback(getPeers(awareness));\n awareness.on(\"change\", emit);\n emit();\n return () => awareness.off(\"change\", emit);\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport type * as Y from \"yjs\";\nimport {\n bindCollab,\n type BindCollabOptions,\n type CollabBinding,\n type ConnectionStatus,\n} from \"./binding\";\n\nexport interface UseCollabOptions extends Omit<BindCollabOptions, \"ydoc\"> {\n ydoc: Y.Doc;\n}\n\nexport interface UseCollabResult {\n status: ConnectionStatus;\n synced: boolean;\n binding: CollabBinding | null;\n}\n\nexport function useCollab(options: UseCollabOptions): UseCollabResult {\n const { ydoc, url, name, token, user } = options;\n const [status, setStatus] = useState<ConnectionStatus>(\"connecting\");\n const [synced, setSynced] = useState(false);\n const bindingRef = useRef<CollabBinding | null>(null);\n\n useEffect(() => {\n const binding = bindCollab({\n ydoc,\n url,\n name,\n token,\n user,\n onStatus: (s) => setStatus(s),\n onSynced: (s) => setSynced(s),\n });\n bindingRef.current = binding;\n return () => {\n binding.destroy();\n bindingRef.current = null;\n setStatus(\"disconnected\");\n setSynced(false);\n };\n // Re-bind when document identity or connection params change. `user` is\n // updated via setUser below so changes don't tear down the socket.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [ydoc, url, name, token]);\n\n useEffect(() => {\n bindingRef.current?.setUser(user ?? null);\n }, [user?.name, user?.color]);\n\n return { status, synced, binding: bindingRef.current };\n}\n"],"mappings":";AACA,SAAS,0BAA0B;AA2B5B,SAAS,WAAW,MAAwC;AACjE,QAAM,WAAW,IAAI,mBAAmB;AAAA,IACtC,KAAK,KAAK;AAAA,IACV,MAAM,KAAK;AAAA,IACX,UAAU,KAAK;AAAA,IACf,OAAO,KAAK;AAAA,IACZ,UAAU,KAAK,WACX,CAAC,EAAE,OAAO,MAAM,KAAK,SAAU,MAA0B,IACzD;AAAA,IACJ,UAAU,KAAK,WACX,CAAC,EAAE,MAAM,MAAM,KAAK,SAAU,KAAK,IACnC;AAAA,EACN,CAAC;AAED,MAAI,KAAK,MAAM;AACb,aAAS,WAAW,mBAAmB,QAAQ,KAAK,IAAI;AAAA,EAC1D;AAEA,MAAI,YAAY;AAEhB,SAAO;AAAA,IACL;AAAA,IACA,IAAI,YAAY;AACd,YAAM,IAAI,SAAS;AACnB,UAAI,CAAC,GAAG;AACN,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,IACA,QAAQ,MAAM;AACZ,UAAI,UAAW;AACf,eAAS,WAAW,mBAAmB,QAAQ,IAAI;AAAA,IACrD;AAAA,IACA,UAAU;AACR,UAAI,UAAW;AACf,kBAAY;AACZ,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF;AACF;;;AC7DO,SAAS,SAAS,WAAmC;AAC1D,QAAM,QAAqB,CAAC;AAC5B,YAAU,UAAU,EAAE,QAAQ,CAAC,OAAO,aAAa;AACjD,QAAI,aAAa,UAAU,SAAU;AACrC,UAAM,KAAK;AAAA,MACT;AAAA,MACA,MAAO,MAAgC;AAAA,IACzC,CAAC;AAAA,EACH,CAAC;AACD,SAAO;AACT;AAEO,SAAS,aACd,WACA,UACY;AACZ,QAAM,OAAO,MAAM,SAAS,SAAS,SAAS,CAAC;AAC/C,YAAU,GAAG,UAAU,IAAI;AAC3B,OAAK;AACL,SAAO,MAAM,UAAU,IAAI,UAAU,IAAI;AAC3C;;;AC5BA,SAAS,WAAW,QAAQ,gBAAgB;AAmBrC,SAAS,UAAU,SAA4C;AACpE,QAAM,EAAE,MAAM,KAAK,MAAM,OAAO,KAAK,IAAI;AACzC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA2B,YAAY;AACnE,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,aAAa,OAA6B,IAAI;AAEpD,YAAU,MAAM;AACd,UAAM,UAAU,WAAW;AAAA,MACzB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,CAAC,MAAM,UAAU,CAAC;AAAA,MAC5B,UAAU,CAAC,MAAM,UAAU,CAAC;AAAA,IAC9B,CAAC;AACD,eAAW,UAAU;AACrB,WAAO,MAAM;AACX,cAAQ,QAAQ;AAChB,iBAAW,UAAU;AACrB,gBAAU,cAAc;AACxB,gBAAU,KAAK;AAAA,IACjB;AAAA,EAIF,GAAG,CAAC,MAAM,KAAK,MAAM,KAAK,CAAC;AAE3B,YAAU,MAAM;AACd,eAAW,SAAS,QAAQ,QAAQ,IAAI;AAAA,EAC1C,GAAG,CAAC,MAAM,MAAM,MAAM,KAAK,CAAC;AAE5B,SAAO,EAAE,QAAQ,QAAQ,SAAS,WAAW,QAAQ;AACvD;","names":[]}
1
+ {"version":3,"sources":["../src/binding.ts","../src/awareness.ts","../src/useCollab.ts"],"sourcesContent":["import type * as Y from \"yjs\";\nimport { HocuspocusProvider } from \"@hocuspocus/provider\";\nimport type { Awareness } from \"y-protocols/awareness\";\n\nexport type ConnectionStatus = \"connecting\" | \"connected\" | \"disconnected\";\n\nexport interface CollabUser {\n name: string;\n color: string;\n}\n\nexport interface BindCollabOptions {\n ydoc: Y.Doc;\n url: string;\n name: string;\n token?: string;\n user?: CollabUser;\n onStatus?: (status: ConnectionStatus) => void;\n onSynced?: (synced: boolean) => void;\n}\n\nexport interface CollabBinding {\n readonly provider: HocuspocusProvider;\n readonly awareness: Awareness;\n setUser(user: CollabUser | null): void;\n destroy(): void;\n}\n\nexport function bindCollab(opts: BindCollabOptions): CollabBinding {\n const provider = new HocuspocusProvider({\n url: opts.url,\n name: opts.name,\n document: opts.ydoc,\n token: opts.token,\n onStatus: opts.onStatus\n ? ({ status }) => opts.onStatus!(status as ConnectionStatus)\n : undefined,\n onSynced: opts.onSynced\n ? ({ state }) => opts.onSynced!(state)\n : undefined,\n });\n\n if (opts.user) {\n provider.awareness?.setLocalStateField(\"user\", opts.user);\n }\n\n let destroyed = false;\n\n return {\n provider,\n get awareness() {\n const a = provider.awareness;\n if (!a) {\n throw new Error(\n \"[@sofereditor/collab] Awareness not yet available (provider not connected).\",\n );\n }\n return a;\n },\n setUser(user) {\n if (destroyed) return;\n provider.awareness?.setLocalStateField(\"user\", user);\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n provider.destroy();\n },\n };\n}\n","import type { Awareness } from \"y-protocols/awareness\";\nimport type { EncodedCursor } from \"@sofereditor/core\";\nimport type { CollabUser } from \"./binding\";\n\nexport interface PeerState {\n clientId: number;\n user?: CollabUser;\n /** Cursor/seleção remota codificada (Y.RelativePosition + blockId). */\n cursor?: EncodedCursor;\n}\n\nexport function getPeers(awareness: Awareness): PeerState[] {\n const peers: PeerState[] = [];\n awareness.getStates().forEach((state, clientId) => {\n if (clientId === awareness.clientID) return;\n const s = state as { user?: CollabUser; cursor?: EncodedCursor };\n peers.push({\n clientId,\n user: s.user,\n cursor: s.cursor ?? undefined,\n });\n });\n return peers;\n}\n\nexport function observePeers(\n awareness: Awareness,\n callback: (peers: PeerState[]) => void,\n): () => void {\n const emit = () => callback(getPeers(awareness));\n awareness.on(\"change\", emit);\n emit();\n return () => awareness.off(\"change\", emit);\n}\n\n/**\n * Publica (ou limpa, com null) o cursor/seleção local no awareness. O editor\n * chama isto a cada mudança de seleção (com throttle). Os peers leem via\n * `getPeers`/`observePeers`.\n */\nexport function setLocalCursor(\n awareness: Awareness,\n cursor: EncodedCursor | null,\n): void {\n awareness.setLocalStateField(\"cursor\", cursor);\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport type * as Y from \"yjs\";\nimport {\n bindCollab,\n type BindCollabOptions,\n type CollabBinding,\n type ConnectionStatus,\n} from \"./binding\";\n\nexport interface UseCollabOptions extends Omit<BindCollabOptions, \"ydoc\"> {\n ydoc: Y.Doc;\n}\n\nexport interface UseCollabResult {\n status: ConnectionStatus;\n synced: boolean;\n binding: CollabBinding | null;\n}\n\nexport function useCollab(options: UseCollabOptions): UseCollabResult {\n const { ydoc, url, name, token, user } = options;\n const [status, setStatus] = useState<ConnectionStatus>(\"connecting\");\n const [synced, setSynced] = useState(false);\n const bindingRef = useRef<CollabBinding | null>(null);\n\n useEffect(() => {\n const binding = bindCollab({\n ydoc,\n url,\n name,\n token,\n user,\n onStatus: (s) => setStatus(s),\n onSynced: (s) => setSynced(s),\n });\n bindingRef.current = binding;\n return () => {\n binding.destroy();\n bindingRef.current = null;\n setStatus(\"disconnected\");\n setSynced(false);\n };\n // Re-bind when document identity or connection params change. `user` is\n // updated via setUser below so changes don't tear down the socket.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [ydoc, url, name, token]);\n\n useEffect(() => {\n bindingRef.current?.setUser(user ?? null);\n }, [user?.name, user?.color]);\n\n return { status, synced, binding: bindingRef.current };\n}\n"],"mappings":";AACA,SAAS,0BAA0B;AA2B5B,SAAS,WAAW,MAAwC;AACjE,QAAM,WAAW,IAAI,mBAAmB;AAAA,IACtC,KAAK,KAAK;AAAA,IACV,MAAM,KAAK;AAAA,IACX,UAAU,KAAK;AAAA,IACf,OAAO,KAAK;AAAA,IACZ,UAAU,KAAK,WACX,CAAC,EAAE,OAAO,MAAM,KAAK,SAAU,MAA0B,IACzD;AAAA,IACJ,UAAU,KAAK,WACX,CAAC,EAAE,MAAM,MAAM,KAAK,SAAU,KAAK,IACnC;AAAA,EACN,CAAC;AAED,MAAI,KAAK,MAAM;AACb,aAAS,WAAW,mBAAmB,QAAQ,KAAK,IAAI;AAAA,EAC1D;AAEA,MAAI,YAAY;AAEhB,SAAO;AAAA,IACL;AAAA,IACA,IAAI,YAAY;AACd,YAAM,IAAI,SAAS;AACnB,UAAI,CAAC,GAAG;AACN,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,IACA,QAAQ,MAAM;AACZ,UAAI,UAAW;AACf,eAAS,WAAW,mBAAmB,QAAQ,IAAI;AAAA,IACrD;AAAA,IACA,UAAU;AACR,UAAI,UAAW;AACf,kBAAY;AACZ,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF;AACF;;;AC1DO,SAAS,SAAS,WAAmC;AAC1D,QAAM,QAAqB,CAAC;AAC5B,YAAU,UAAU,EAAE,QAAQ,CAAC,OAAO,aAAa;AACjD,QAAI,aAAa,UAAU,SAAU;AACrC,UAAM,IAAI;AACV,UAAM,KAAK;AAAA,MACT;AAAA,MACA,MAAM,EAAE;AAAA,MACR,QAAQ,EAAE,UAAU;AAAA,IACtB,CAAC;AAAA,EACH,CAAC;AACD,SAAO;AACT;AAEO,SAAS,aACd,WACA,UACY;AACZ,QAAM,OAAO,MAAM,SAAS,SAAS,SAAS,CAAC;AAC/C,YAAU,GAAG,UAAU,IAAI;AAC3B,OAAK;AACL,SAAO,MAAM,UAAU,IAAI,UAAU,IAAI;AAC3C;AAOO,SAAS,eACd,WACA,QACM;AACN,YAAU,mBAAmB,UAAU,MAAM;AAC/C;;;AC7CA,SAAS,WAAW,QAAQ,gBAAgB;AAmBrC,SAAS,UAAU,SAA4C;AACpE,QAAM,EAAE,MAAM,KAAK,MAAM,OAAO,KAAK,IAAI;AACzC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA2B,YAAY;AACnE,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,aAAa,OAA6B,IAAI;AAEpD,YAAU,MAAM;AACd,UAAM,UAAU,WAAW;AAAA,MACzB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,CAAC,MAAM,UAAU,CAAC;AAAA,MAC5B,UAAU,CAAC,MAAM,UAAU,CAAC;AAAA,IAC9B,CAAC;AACD,eAAW,UAAU;AACrB,WAAO,MAAM;AACX,cAAQ,QAAQ;AAChB,iBAAW,UAAU;AACrB,gBAAU,cAAc;AACxB,gBAAU,KAAK;AAAA,IACjB;AAAA,EAIF,GAAG,CAAC,MAAM,KAAK,MAAM,KAAK,CAAC;AAE3B,YAAU,MAAM;AACd,eAAW,SAAS,QAAQ,QAAQ,IAAI;AAAA,EAC1C,GAAG,CAAC,MAAM,MAAM,MAAM,KAAK,CAAC;AAE5B,SAAO,EAAE,QAAQ,QAAQ,SAAS,WAAW,QAAQ;AACvD;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sofereditor/collab",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "@sofereditor/collab — Hocuspocus binding for real-time collaboration with the Sofer editor. Bidirectional Y.Doc sync + awareness.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "author": "Rafael Marreca",
@@ -22,19 +22,37 @@
22
22
  "sofer"
23
23
  ],
24
24
  "type": "module",
25
- "main": "./dist/index.cjs",
26
- "module": "./dist/index.js",
27
- "types": "./dist/index.d.ts",
25
+ "main": "./src/index.ts",
26
+ "module": "./src/index.ts",
27
+ "types": "./src/index.ts",
28
28
  "exports": {
29
29
  ".": {
30
- "types": "./dist/index.d.ts",
31
- "import": "./dist/index.js",
32
- "require": "./dist/index.cjs"
30
+ "types": "./src/index.ts",
31
+ "import": "./src/index.ts",
32
+ "require": "./src/index.ts"
33
+ }
34
+ },
35
+ "publishConfig": {
36
+ "main": "./dist/index.cjs",
37
+ "module": "./dist/index.js",
38
+ "types": "./dist/index.d.ts",
39
+ "exports": {
40
+ ".": {
41
+ "types": "./dist/index.d.ts",
42
+ "import": "./dist/index.js",
43
+ "require": "./dist/index.cjs"
44
+ }
33
45
  }
34
46
  },
35
47
  "files": [
36
48
  "dist"
37
49
  ],
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "dev": "tsup --watch",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run --passWithNoTests"
55
+ },
38
56
  "peerDependencies": {
39
57
  "react": ">=18"
40
58
  },
@@ -44,10 +62,10 @@
44
62
  }
45
63
  },
46
64
  "dependencies": {
65
+ "@sofereditor/core": "^0.3.0",
47
66
  "@hocuspocus/provider": "^3.4.4",
48
67
  "y-protocols": "^1.0.6",
49
- "yjs": "^13.6.20",
50
- "@sofereditor/core": "0.1.0"
68
+ "yjs": "^13.6.20"
51
69
  },
52
70
  "devDependencies": {
53
71
  "@types/react": "^18.3.12",
@@ -55,11 +73,5 @@
55
73
  "tsup": "^8.3.5",
56
74
  "typescript": "^5.7.2",
57
75
  "vitest": "^2.1.8"
58
- },
59
- "scripts": {
60
- "build": "tsup",
61
- "dev": "tsup --watch",
62
- "typecheck": "tsc --noEmit",
63
- "test": "vitest run --passWithNoTests"
64
76
  }
65
- }
77
+ }