@symbo.ls/sdk 3.1.2 → 3.2.3

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 (65) hide show
  1. package/README.md +2 -2
  2. package/dist/cjs/config/environment.js +5 -21
  3. package/dist/cjs/index.js +6 -26
  4. package/dist/cjs/services/AIService.js +3 -3
  5. package/dist/cjs/services/CollabService.js +420 -0
  6. package/dist/cjs/services/CoreService.js +651 -107
  7. package/dist/cjs/services/SocketService.js +207 -59
  8. package/dist/cjs/services/index.js +5 -13
  9. package/dist/cjs/state/RootStateManager.js +86 -0
  10. package/dist/cjs/state/rootEventBus.js +65 -0
  11. package/dist/cjs/utils/CollabClient.js +157 -0
  12. package/dist/cjs/utils/TokenManager.js +62 -27
  13. package/dist/cjs/utils/jsonDiff.js +103 -0
  14. package/dist/cjs/utils/services.js +129 -88
  15. package/dist/cjs/utils/symstoryClient.js +5 -5
  16. package/dist/esm/config/environment.js +5 -21
  17. package/dist/esm/index.js +20459 -9286
  18. package/dist/esm/services/AIService.js +3 -3
  19. package/dist/esm/services/BasedService.js +5 -21
  20. package/dist/esm/services/CollabService.js +18028 -0
  21. package/dist/esm/services/CoreService.js +718 -155
  22. package/dist/esm/services/SocketService.js +323 -58
  23. package/dist/esm/services/SymstoryService.js +10 -26
  24. package/dist/esm/services/index.js +20305 -9158
  25. package/dist/esm/state/RootStateManager.js +102 -0
  26. package/dist/esm/state/rootEventBus.js +47 -0
  27. package/dist/esm/utils/CollabClient.js +17483 -0
  28. package/dist/esm/utils/TokenManager.js +62 -27
  29. package/dist/esm/utils/jsonDiff.js +6096 -0
  30. package/dist/esm/utils/services.js +129 -88
  31. package/dist/esm/utils/symstoryClient.js +10 -26
  32. package/dist/node/config/environment.js +5 -21
  33. package/dist/node/index.js +10 -34
  34. package/dist/node/services/AIService.js +3 -3
  35. package/dist/node/services/CollabService.js +401 -0
  36. package/dist/node/services/CoreService.js +651 -107
  37. package/dist/node/services/SocketService.js +197 -59
  38. package/dist/node/services/index.js +5 -13
  39. package/dist/node/state/RootStateManager.js +57 -0
  40. package/dist/node/state/rootEventBus.js +46 -0
  41. package/dist/node/utils/CollabClient.js +128 -0
  42. package/dist/node/utils/TokenManager.js +62 -27
  43. package/dist/node/utils/jsonDiff.js +74 -0
  44. package/dist/node/utils/services.js +129 -88
  45. package/dist/node/utils/symstoryClient.js +5 -5
  46. package/package.json +12 -6
  47. package/src/config/environment.js +5 -19
  48. package/src/index.js +9 -31
  49. package/src/services/AIService.js +3 -3
  50. package/src/services/BasedService.js +1 -0
  51. package/src/services/CollabService.js +491 -0
  52. package/src/services/CoreService.js +715 -110
  53. package/src/services/SocketService.js +227 -59
  54. package/src/services/index.js +6 -13
  55. package/src/state/RootStateManager.js +71 -0
  56. package/src/state/rootEventBus.js +48 -0
  57. package/src/utils/CollabClient.js +161 -0
  58. package/src/utils/TokenManager.js +68 -30
  59. package/src/utils/jsonDiff.js +109 -0
  60. package/src/utils/services.js +140 -88
  61. package/src/utils/symstoryClient.js +5 -5
  62. package/dist/cjs/services/SocketIOService.js +0 -307
  63. package/dist/esm/services/SocketIOService.js +0 -470
  64. package/dist/node/services/SocketIOService.js +0 -278
  65. package/src/services/SocketIOService.js +0 -334
@@ -1,4 +1,9 @@
1
- import { BaseService } from "./BaseService";
1
+ import { connect, send, disconnect } from "@symbo.ls/socket/client.js";
2
+ import { BaseService } from "./BaseService.js";
3
+ import * as utils from "@domql/utils";
4
+ import { router } from "@symbo.ls/router";
5
+ import environment from "../config/environment.js";
6
+ const { deepStringify, deepDestringify, isString } = utils.default || utils;
2
7
  class SocketService extends BaseService {
3
8
  constructor(config) {
4
9
  super(config);
@@ -7,24 +12,39 @@ class SocketService extends BaseService {
7
12
  this._maxReconnectAttempts = (config == null ? void 0 : config.maxReconnectAttempts) || 5;
8
13
  this._reconnectDelay = (config == null ? void 0 : config.reconnectDelay) || 1e3;
9
14
  this._handlers = /* @__PURE__ */ new Map();
15
+ this._sessionId = Math.random();
16
+ this._ignoreSync = [
17
+ "userId",
18
+ "username",
19
+ "usersName",
20
+ "email",
21
+ "projects",
22
+ "feedbacks",
23
+ "userRoles",
24
+ "loading",
25
+ "appKey",
26
+ "projectName",
27
+ "followingUser",
28
+ "activeProject",
29
+ "user",
30
+ "sessionId",
31
+ "clients"
32
+ ];
10
33
  }
11
- async init({ context, options }) {
34
+ init() {
12
35
  try {
13
- this._context = context;
14
- this._options = options;
15
- const { authToken } = context;
16
- const { socketUrl } = context.socket || {};
36
+ const { _context, _options } = this;
37
+ const socketUrl = environment.socketUrl || _options.socketUrl;
17
38
  if (!socketUrl) {
18
39
  throw new Error("Socket URL is required");
19
40
  }
20
41
  this._info = {
21
42
  config: {
22
43
  url: socketUrl,
23
- hasToken: Boolean(authToken),
44
+ hasToken: Boolean(_context.authToken),
24
45
  status: "initializing"
25
46
  }
26
47
  };
27
- await this.connect();
28
48
  this._setReady();
29
49
  } catch (error) {
30
50
  this._setError(error);
@@ -32,76 +52,189 @@ class SocketService extends BaseService {
32
52
  }
33
53
  }
34
54
  connect() {
55
+ var _a, _b, _c, _d, _e, _f, _g;
35
56
  try {
36
- const { socketUrl, authToken } = this._context.socket || {};
37
- this._socket = new window.WebSocket(socketUrl);
38
- this._socket.onopen = () => {
39
- this._reconnectAttempts = 0;
40
- this._updateStatus("connected");
41
- if (authToken) {
42
- this.send("auth", { token: authToken });
43
- }
44
- };
45
- this._socket.onclose = () => {
46
- this._updateStatus("disconnected");
47
- this._handleReconnect();
48
- };
49
- this._socket.onerror = (error) => {
50
- this._updateStatus("error");
51
- this._setError(error);
52
- };
53
- this._socket.onmessage = (event) => {
54
- this._handleMessage(event.data);
57
+ if (this._socket && ["connected", "connecting"].includes((_b = (_a = this._info) == null ? void 0 : _a.config) == null ? void 0 : _b.status)) {
58
+ console.warn(
59
+ "Socket connection already exists:",
60
+ (_d = (_c = this._info) == null ? void 0 : _c.config) == null ? void 0 : _d.status
61
+ );
62
+ return true;
63
+ }
64
+ const { _context } = this;
65
+ if (!_context.appKey) {
66
+ throw new Error("App key is required");
67
+ }
68
+ this._updateStatus("connecting");
69
+ const config = {
70
+ source: "platform",
71
+ userId: (_e = _context.user) == null ? void 0 : _e.id,
72
+ socketUrl: this._info.config.url,
73
+ location: window.location.host,
74
+ // onConnect: () => {
75
+ // console.log('waz')
76
+ // },
77
+ onChange: this._handleMessage.bind(this),
78
+ sessionId: this._sessionId,
79
+ usersName: (_f = _context.user) == null ? void 0 : _f.name,
80
+ route: window.location.pathname,
81
+ onDisconnect: this._handleDisconnect.bind(this)
55
82
  };
83
+ if (this._socket) {
84
+ this.destroy();
85
+ }
86
+ this._socket = connect(_context.appKey, config);
87
+ this._updateStatus("connected");
88
+ if (environment.isDevelopment) {
89
+ console.log("Socket connection established:", {
90
+ appKey: _context.appKey,
91
+ userId: (_g = _context.user) == null ? void 0 : _g.id,
92
+ sessionId: this._sessionId,
93
+ url: this._info.config.url
94
+ });
95
+ }
96
+ return true;
56
97
  } catch (error) {
98
+ this._updateStatus("failed");
99
+ console.error("Socket connection failed:", error);
57
100
  throw new Error(`Socket connection failed: ${error.message}`);
58
101
  }
59
102
  }
60
- // Send message to socket server
61
- send(type, data) {
103
+ send(type, data, opts = {}) {
104
+ var _a, _b;
62
105
  this._requireReady();
63
- if (!this._socket || this._socket.readyState !== window.WebSocket.OPEN) {
106
+ if (!this._socket) {
64
107
  throw new Error("Socket is not connected");
65
108
  }
66
- this._socket.send(JSON.stringify({ type, data }));
67
- }
68
- // Subscribe to socket events
69
- subscribe(type, handler) {
70
- if (!this._handlers.has(type)) {
71
- this._handlers.set(type, /* @__PURE__ */ new Set());
72
- }
73
- this._handlers.get(type).add(handler);
74
- return () => {
75
- const handlers = this._handlers.get(type);
76
- if (handlers) {
77
- handlers.delete(handler);
78
- if (handlers.size === 0) {
79
- this._handlers.delete(type);
80
- }
81
- }
109
+ const payload = {
110
+ sessionId: this._sessionId,
111
+ userId: (_a = this._context.user) == null ? void 0 : _a.id,
112
+ usersName: (_b = this._context.user) == null ? void 0 : _b.name,
113
+ ...data
82
114
  };
115
+ send.call(
116
+ this._socket,
117
+ type,
118
+ opts.useDeepStringify ? deepStringify(payload) : payload
119
+ );
83
120
  }
84
- subscribeChannel(type, handler) {
85
- return this.subscribe(type, handler);
86
- }
87
- // Private methods
88
- _handleMessage(rawData) {
121
+ _handleMessage(event, data) {
89
122
  try {
90
- const { type, data } = JSON.parse(rawData);
91
- const handlers = this._handlers.get(type);
123
+ const d = isString(data) ? deepDestringify(JSON.parse(data)) : data;
124
+ if (this._sessionId === d.sessionId) {
125
+ return;
126
+ }
127
+ const handlers = this._handlers.get(event);
92
128
  if (handlers) {
93
- handlers.forEach((handler) => handler(data));
129
+ handlers.forEach((handler) => handler(d));
130
+ }
131
+ switch (event) {
132
+ case "change":
133
+ this._handleChangeEvent(d);
134
+ break;
135
+ case "clients":
136
+ this._handleClientsEvent(d);
137
+ break;
138
+ case "route":
139
+ this._handleRouteEvent(d);
140
+ break;
141
+ default:
142
+ break;
94
143
  }
95
144
  } catch (error) {
96
145
  this._setError(new Error(`Failed to handle message: ${error.message}`));
97
146
  }
98
147
  }
148
+ _handleChangeEvent(data) {
149
+ const { type, changes, version } = data;
150
+ if (version) {
151
+ this._context.state.version = version;
152
+ }
153
+ if (changes) {
154
+ window.requestAnimationFrame(async () => {
155
+ await this._context.state.setPathCollection(changes, {
156
+ preventReplace: type === "canvas",
157
+ preventUpdate: true,
158
+ fromSocket: true,
159
+ userId: data.userId,
160
+ changes
161
+ });
162
+ });
163
+ }
164
+ }
165
+ _handleClientsEvent(data) {
166
+ const { root } = this._context.state;
167
+ root.replace(
168
+ { clients: data },
169
+ {
170
+ fromSocket: true,
171
+ preventUpdate: true
172
+ }
173
+ );
174
+ }
175
+ _handleRouteEvent(data) {
176
+ const { element } = this._context;
177
+ const { state } = this._context;
178
+ if (data.userId && data.type === "routeChanged") {
179
+ const isModalOpen = this.getWindow("modal");
180
+ const isFollowing = state.followingUser === data.userId;
181
+ const isRouteSyncEnabled = element.getUserSettings("presentMode") && data.userId === state.userId;
182
+ if ((isFollowing || isRouteSyncEnabled) && !isModalOpen) {
183
+ router(
184
+ data.route,
185
+ element.__ref.root,
186
+ {},
187
+ {
188
+ fromSocket: true,
189
+ updateStateOptions: {
190
+ fromSocket: true,
191
+ preventStateUpdateListener: 1
192
+ // !isModalRoute(data.route, element)
193
+ }
194
+ }
195
+ );
196
+ }
197
+ } else if (data.reload) {
198
+ window.location.reload();
199
+ } else if (data.route && data.type === "routeForced") {
200
+ router(
201
+ data.route,
202
+ element.__ref.root,
203
+ {},
204
+ {
205
+ fromSocket: true,
206
+ updateStateOptions: {
207
+ fromSocket: true
208
+ }
209
+ }
210
+ );
211
+ } else if (data.componentKey) {
212
+ if (!element.getData("components")[data.componentKey]) {
213
+ return;
214
+ }
215
+ element.activateSelected(data.componentKey);
216
+ }
217
+ }
218
+ _handleDisconnect() {
219
+ this._updateStatus("disconnected");
220
+ this._handleReconnect();
221
+ }
99
222
  _handleReconnect() {
100
223
  if (this._reconnectAttempts < this._maxReconnectAttempts) {
101
224
  this._reconnectAttempts++;
102
225
  this._updateStatus("reconnecting");
103
226
  setTimeout(() => {
104
- this.connect();
227
+ try {
228
+ const connected = this.connect();
229
+ if (connected) {
230
+ this._reconnectAttempts = 0;
231
+ } else {
232
+ this._handleReconnect();
233
+ }
234
+ } catch (error) {
235
+ console.error("Reconnection failed:", error);
236
+ this._handleReconnect();
237
+ }
105
238
  }, this._reconnectDelay * this._reconnectAttempts);
106
239
  } else {
107
240
  this._updateStatus("failed");
@@ -117,18 +250,23 @@ class SocketService extends BaseService {
117
250
  }
118
251
  };
119
252
  }
120
- // Cleanup
121
253
  destroy() {
122
254
  if (this._socket) {
123
- this._socket.close();
255
+ disconnect.call(this._socket);
124
256
  this._socket = null;
125
257
  }
126
258
  this._handlers.clear();
127
259
  this._setReady(false);
128
260
  }
261
+ reconnect() {
262
+ this.destroy();
263
+ this.connect();
264
+ }
129
265
  _checkRequiredContext() {
130
- const { socket } = this._context;
131
- return Boolean((socket == null ? void 0 : socket.socketUrl) && this._socket);
266
+ var _a, _b;
267
+ return Boolean(
268
+ ((_a = this._context) == null ? void 0 : _a.appKey) && ((_b = this._context) == null ? void 0 : _b.authToken) && this._socket
269
+ );
132
270
  }
133
271
  isReady() {
134
272
  if (this._checkRequiredContext()) {
@@ -1,23 +1,15 @@
1
- import { SymstoryService } from "./SymstoryService.js";
2
1
  import { AuthService } from "./AuthService.js";
3
- import { AIService } from "./AIService.js";
4
- import { SocketService } from "./SocketIOService.js";
5
2
  import { CoreService } from "./CoreService.js";
3
+ import { CollabService } from "./CollabService.js";
6
4
  const createService = (ServiceClass, config) => new ServiceClass(config);
7
- const createSymstoryService = (config) => createService(SymstoryService, config);
8
5
  const createAuthService = (config) => createService(AuthService, config);
9
- const createAIService = (config) => createService(AIService, config);
10
- const createSocketService = (config) => createService(SocketService, config);
11
6
  const createCoreService = (config) => createService(CoreService, config);
7
+ const createCollabService = (config) => createService(CollabService, config);
12
8
  export {
13
- AIService,
14
9
  AuthService,
10
+ CollabService,
15
11
  CoreService,
16
- SocketService,
17
- SymstoryService,
18
- createAIService,
19
12
  createAuthService,
20
- createCoreService,
21
- createSocketService,
22
- createSymstoryService
13
+ createCollabService,
14
+ createCoreService
23
15
  };
@@ -0,0 +1,57 @@
1
+ import * as utils from "@domql/utils";
2
+ import { rootBus } from "./rootEventBus.js";
3
+ const { isFunction } = utils.default || utils;
4
+ class RootStateManager {
5
+ constructor(rootState) {
6
+ this._rootState = rootState;
7
+ }
8
+ /**
9
+ * Apply change tuples to the root state using the built-in setPathCollection
10
+ * of Symbo.ls APP state tree.
11
+ *
12
+ * @param {Array} changes – eg. ['update', ['foo'], 'bar']
13
+ * @param {Object} opts – forwarded to setPathCollection
14
+ */
15
+ applyChanges(changes = [], opts = {}) {
16
+ if (!this._rootState || !isFunction(this._rootState.setPathCollection)) {
17
+ return;
18
+ }
19
+ const result = this._rootState.setPathCollection(changes, {
20
+ preventUpdate: true,
21
+ ...opts
22
+ });
23
+ try {
24
+ const changedKeys = /* @__PURE__ */ new Set();
25
+ changes.forEach((tuple) => {
26
+ const [, path = []] = tuple;
27
+ if (!Array.isArray(path) || !path.length) {
28
+ return;
29
+ }
30
+ if (path[0] === "components" && typeof path[1] === "string") {
31
+ changedKeys.add(path[1]);
32
+ }
33
+ if (path[0] === "schema" && path[1] === "components" && typeof path[2] === "string") {
34
+ changedKeys.add(path[2]);
35
+ }
36
+ });
37
+ if (changedKeys.size) {
38
+ console.log("emit components:changed", [...changedKeys]);
39
+ rootBus.emit("components:changed", [...changedKeys]);
40
+ }
41
+ } catch (err) {
42
+ console.error("[RootStateManager] emit components:changed failed", err);
43
+ }
44
+ return result;
45
+ }
46
+ setVersion(v) {
47
+ if (this._rootState) {
48
+ this._rootState.version = v;
49
+ }
50
+ }
51
+ get root() {
52
+ return this._rootState;
53
+ }
54
+ }
55
+ export {
56
+ RootStateManager
57
+ };
@@ -0,0 +1,46 @@
1
+ const getGlobalBus = () => {
2
+ if (globalThis.__SMBLS_ROOT_BUS__) {
3
+ return globalThis.__SMBLS_ROOT_BUS__;
4
+ }
5
+ const events = {};
6
+ const bus = {
7
+ on(event, handler) {
8
+ (events[event] ||= []).push(handler);
9
+ },
10
+ off(event, handler) {
11
+ const list = events[event];
12
+ if (!list) {
13
+ return;
14
+ }
15
+ const idx = list.indexOf(handler);
16
+ if (idx !== -1) {
17
+ list.splice(idx, 1);
18
+ }
19
+ },
20
+ emit(event, payload) {
21
+ const list = events[event];
22
+ if (!list || !list.length) {
23
+ return;
24
+ }
25
+ list.slice().forEach((fn) => {
26
+ try {
27
+ fn(payload);
28
+ } catch (err) {
29
+ console.error("[rootBus] handler error for", event, err);
30
+ }
31
+ });
32
+ }
33
+ };
34
+ Object.defineProperty(bus, "_listeners", {
35
+ value: events,
36
+ enumerable: false
37
+ });
38
+ globalThis.__SMBLS_ROOT_BUS__ = bus;
39
+ return bus;
40
+ };
41
+ const rootBus = getGlobalBus();
42
+ var rootEventBus_default = rootBus;
43
+ export {
44
+ rootEventBus_default as default,
45
+ rootBus
46
+ };
@@ -0,0 +1,128 @@
1
+ import { io } from "socket.io-client";
2
+ import * as Y from "yjs";
3
+ import { IndexeddbPersistence } from "y-indexeddb";
4
+ import Dexie from "dexie";
5
+ import { nanoid } from "nanoid";
6
+ import environment from "../config/environment.js";
7
+ import { diffJson, applyOpsToJson } from "./jsonDiff.js";
8
+ class CollabClient {
9
+ /* public fields */
10
+ socket = null;
11
+ ydoc = null;
12
+ branch = "main";
13
+ live = false;
14
+ projectId = null;
15
+ jwt = null;
16
+ /* private state */
17
+ _buffer = [];
18
+ _flushTimer = null;
19
+ _clientId = nanoid();
20
+ _outboxStore = null;
21
+ // Dexie table
22
+ _readyResolve;
23
+ ready = new Promise((res) => this._readyResolve = res);
24
+ constructor({ jwt, projectId, branch = "main", live = false }) {
25
+ Object.assign(this, { jwt, projectId, branch, live });
26
+ this.ydoc = new Y.Doc();
27
+ new IndexeddbPersistence(`${projectId}:${branch}`, this.ydoc);
28
+ this._outboxStore = createDexieOutbox(`${projectId}:${branch}`);
29
+ this.socket = io(environment.socketUrl, {
30
+ path: "/collab-socket",
31
+ transports: ["websocket"],
32
+ auth: { token: jwt, projectId, branch, live },
33
+ reconnectionAttempts: Infinity,
34
+ reconnectionDelayMax: 4e3
35
+ });
36
+ this.socket.on("snapshot", this._onSnapshot).on("ops", this._onOps).on("commit", this._onCommit).on("liveMode", (flag) => {
37
+ this.live = flag;
38
+ }).on("connect", this._onConnect).on("error", (e) => console.warn("[collab] socket error", e));
39
+ this._prevJson = this.ydoc.getMap("root").toJSON();
40
+ this.ydoc.on("afterTransaction", (tr) => {
41
+ if (tr.origin === "remote") {
42
+ return;
43
+ }
44
+ const currentJson = this.ydoc.getMap("root").toJSON();
45
+ const ops = diffJson(this._prevJson, currentJson);
46
+ this._prevJson = currentJson;
47
+ if (!ops.length) {
48
+ return;
49
+ }
50
+ this._queueOps(ops);
51
+ });
52
+ }
53
+ /* ---------- public helpers ---------- */
54
+ toggleLive(flag) {
55
+ this.socket.emit("toggleLive", Boolean(flag));
56
+ }
57
+ sendCursor(data) {
58
+ this.socket.emit("cursor", data);
59
+ }
60
+ sendPresence(d) {
61
+ this.socket.emit("presence", d);
62
+ }
63
+ /* ---------- private handlers ---------- */
64
+ _onSnapshot = ({
65
+ data
66
+ /* Uint8Array */
67
+ }) => {
68
+ Y.applyUpdate(this.ydoc, Uint8Array.from(data));
69
+ this._prevJson = this.ydoc.getMap("root").toJSON();
70
+ if (typeof this._readyResolve === "function") {
71
+ this._readyResolve();
72
+ this._readyResolve = null;
73
+ }
74
+ };
75
+ _onOps = ({ changes }) => {
76
+ applyOpsToJson(changes, this.ydoc);
77
+ this._prevJson = this.ydoc.getMap("root").toJSON();
78
+ };
79
+ _onCommit = async ({ version }) => {
80
+ await this._outboxStore.clear();
81
+ console.info("[collab] committed", version);
82
+ };
83
+ _onConnect = async () => {
84
+ if (typeof this._readyResolve === "function") {
85
+ this._readyResolve();
86
+ this._readyResolve = null;
87
+ }
88
+ const queued = await this._outboxStore.toArray();
89
+ if (queued.length) {
90
+ this.socket.emit("ops", {
91
+ changes: queued.flatMap((e) => e.ops),
92
+ ts: Date.now(),
93
+ clientId: this._clientId
94
+ });
95
+ await this._outboxStore.clear();
96
+ }
97
+ };
98
+ /* ---------- buffering & debounce ---------- */
99
+ _queueOps(ops) {
100
+ this._buffer.push(...ops);
101
+ this._outboxStore.put({ id: nanoid(), ops });
102
+ if (this.live && this.socket.connected) {
103
+ this._flushNow();
104
+ } else {
105
+ clearTimeout(this._flushTimer);
106
+ this._flushTimer = setTimeout(() => this._flushNow(), 40);
107
+ }
108
+ }
109
+ _flushNow() {
110
+ if (!this._buffer.length || !this.socket.connected) {
111
+ return;
112
+ }
113
+ this.socket.emit("ops", {
114
+ changes: this._buffer,
115
+ ts: Date.now(),
116
+ clientId: this._clientId
117
+ });
118
+ this._buffer.length = 0;
119
+ }
120
+ }
121
+ function createDexieOutbox(name) {
122
+ const db = new Dexie(`collab-${name}`);
123
+ db.version(1).stores({ outbox: "id, ops" });
124
+ return db.table("outbox");
125
+ }
126
+ export {
127
+ CollabClient
128
+ };
@@ -129,7 +129,28 @@ class TokenManager {
129
129
  return true;
130
130
  }
131
131
  const now = Date.now();
132
- return now < this.tokens.expiresAt - this.config.refreshBuffer;
132
+ const isValid = now < this.tokens.expiresAt - this.config.refreshBuffer;
133
+ if (!isValid) {
134
+ console.log("[TokenManager] Access token is expired or near expiry:", {
135
+ now: new Date(now).toISOString(),
136
+ expiresAt: new Date(this.tokens.expiresAt).toISOString(),
137
+ refreshBuffer: this.config.refreshBuffer
138
+ });
139
+ }
140
+ return isValid;
141
+ }
142
+ /**
143
+ * Check if access token exists and is not expired (without refresh buffer)
144
+ */
145
+ isAccessTokenActuallyValid() {
146
+ if (!this.tokens.accessToken) {
147
+ return false;
148
+ }
149
+ if (!this.tokens.expiresAt) {
150
+ return true;
151
+ }
152
+ const now = Date.now();
153
+ return now < this.tokens.expiresAt;
133
154
  }
134
155
  /**
135
156
  * Check if tokens exist (regardless of expiry)
@@ -255,40 +276,54 @@ class TokenManager {
255
276
  * Save tokens to storage
256
277
  */
257
278
  saveTokens() {
258
- const { storage } = this;
259
- const keys = this.storageKeys;
260
- if (this.tokens.accessToken) {
261
- storage.setItem(keys.accessToken, this.tokens.accessToken);
262
- }
263
- if (this.tokens.refreshToken) {
264
- storage.setItem(keys.refreshToken, this.tokens.refreshToken);
265
- }
266
- if (this.tokens.expiresAt) {
267
- storage.setItem(keys.expiresAt, this.tokens.expiresAt.toString());
268
- }
269
- if (this.tokens.expiresIn) {
270
- storage.setItem(keys.expiresIn, this.tokens.expiresIn.toString());
279
+ try {
280
+ const { storage } = this;
281
+ const keys = this.storageKeys;
282
+ if (this.tokens.accessToken) {
283
+ storage.setItem(keys.accessToken, this.tokens.accessToken);
284
+ }
285
+ if (this.tokens.refreshToken) {
286
+ storage.setItem(keys.refreshToken, this.tokens.refreshToken);
287
+ }
288
+ if (this.tokens.expiresAt) {
289
+ storage.setItem(keys.expiresAt, this.tokens.expiresAt.toString());
290
+ }
291
+ if (this.tokens.expiresIn) {
292
+ storage.setItem(keys.expiresIn, this.tokens.expiresIn.toString());
293
+ }
294
+ } catch (error) {
295
+ console.error("[TokenManager] Error saving tokens to storage:", error);
271
296
  }
272
297
  }
273
298
  /**
274
299
  * Load tokens from storage
275
300
  */
276
301
  loadTokens() {
277
- const { storage } = this;
278
- const keys = this.storageKeys;
279
- const accessToken = storage.getItem(keys.accessToken);
280
- const refreshToken = storage.getItem(keys.refreshToken);
281
- const expiresAt = storage.getItem(keys.expiresAt);
282
- const expiresIn = storage.getItem(keys.expiresIn);
283
- if (accessToken) {
302
+ try {
303
+ const { storage } = this;
304
+ const keys = this.storageKeys;
305
+ const accessToken = storage.getItem(keys.accessToken);
306
+ const refreshToken = storage.getItem(keys.refreshToken);
307
+ const expiresAt = storage.getItem(keys.expiresAt);
308
+ const expiresIn = storage.getItem(keys.expiresIn);
309
+ if (accessToken) {
310
+ this.tokens = {
311
+ accessToken,
312
+ refreshToken,
313
+ expiresAt: expiresAt ? parseInt(expiresAt, 10) : null,
314
+ expiresIn: expiresIn ? parseInt(expiresIn, 10) : null,
315
+ tokenType: "Bearer"
316
+ };
317
+ this.scheduleRefresh();
318
+ }
319
+ } catch (error) {
320
+ console.error("[TokenManager] Error loading tokens from storage:", error);
284
321
  this.tokens = {
285
- accessToken,
286
- refreshToken,
287
- expiresAt: expiresAt ? parseInt(expiresAt, 10) : null,
288
- expiresIn: expiresIn ? parseInt(expiresIn, 10) : null,
289
- tokenType: "Bearer"
322
+ accessToken: null,
323
+ refreshToken: null,
324
+ expiresAt: null,
325
+ expiresIn: null
290
326
  };
291
- this.scheduleRefresh();
292
327
  }
293
328
  }
294
329
  /**