@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.
- package/README.md +2 -2
- package/dist/cjs/config/environment.js +5 -21
- package/dist/cjs/index.js +6 -26
- package/dist/cjs/services/AIService.js +3 -3
- package/dist/cjs/services/CollabService.js +420 -0
- package/dist/cjs/services/CoreService.js +651 -107
- package/dist/cjs/services/SocketService.js +207 -59
- package/dist/cjs/services/index.js +5 -13
- package/dist/cjs/state/RootStateManager.js +86 -0
- package/dist/cjs/state/rootEventBus.js +65 -0
- package/dist/cjs/utils/CollabClient.js +157 -0
- package/dist/cjs/utils/TokenManager.js +62 -27
- package/dist/cjs/utils/jsonDiff.js +103 -0
- package/dist/cjs/utils/services.js +129 -88
- package/dist/cjs/utils/symstoryClient.js +5 -5
- package/dist/esm/config/environment.js +5 -21
- package/dist/esm/index.js +20459 -9286
- package/dist/esm/services/AIService.js +3 -3
- package/dist/esm/services/BasedService.js +5 -21
- package/dist/esm/services/CollabService.js +18028 -0
- package/dist/esm/services/CoreService.js +718 -155
- package/dist/esm/services/SocketService.js +323 -58
- package/dist/esm/services/SymstoryService.js +10 -26
- package/dist/esm/services/index.js +20305 -9158
- package/dist/esm/state/RootStateManager.js +102 -0
- package/dist/esm/state/rootEventBus.js +47 -0
- package/dist/esm/utils/CollabClient.js +17483 -0
- package/dist/esm/utils/TokenManager.js +62 -27
- package/dist/esm/utils/jsonDiff.js +6096 -0
- package/dist/esm/utils/services.js +129 -88
- package/dist/esm/utils/symstoryClient.js +10 -26
- package/dist/node/config/environment.js +5 -21
- package/dist/node/index.js +10 -34
- package/dist/node/services/AIService.js +3 -3
- package/dist/node/services/CollabService.js +401 -0
- package/dist/node/services/CoreService.js +651 -107
- package/dist/node/services/SocketService.js +197 -59
- package/dist/node/services/index.js +5 -13
- package/dist/node/state/RootStateManager.js +57 -0
- package/dist/node/state/rootEventBus.js +46 -0
- package/dist/node/utils/CollabClient.js +128 -0
- package/dist/node/utils/TokenManager.js +62 -27
- package/dist/node/utils/jsonDiff.js +74 -0
- package/dist/node/utils/services.js +129 -88
- package/dist/node/utils/symstoryClient.js +5 -5
- package/package.json +12 -6
- package/src/config/environment.js +5 -19
- package/src/index.js +9 -31
- package/src/services/AIService.js +3 -3
- package/src/services/BasedService.js +1 -0
- package/src/services/CollabService.js +491 -0
- package/src/services/CoreService.js +715 -110
- package/src/services/SocketService.js +227 -59
- package/src/services/index.js +6 -13
- package/src/state/RootStateManager.js +71 -0
- package/src/state/rootEventBus.js +48 -0
- package/src/utils/CollabClient.js +161 -0
- package/src/utils/TokenManager.js +68 -30
- package/src/utils/jsonDiff.js +109 -0
- package/src/utils/services.js +140 -88
- package/src/utils/symstoryClient.js +5 -5
- package/dist/cjs/services/SocketIOService.js +0 -307
- package/dist/esm/services/SocketIOService.js +0 -470
- package/dist/node/services/SocketIOService.js +0 -278
- package/src/services/SocketIOService.js +0 -334
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
34
|
+
init() {
|
|
12
35
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
61
|
-
|
|
103
|
+
send(type, data, opts = {}) {
|
|
104
|
+
var _a, _b;
|
|
62
105
|
this._requireReady();
|
|
63
|
-
if (!this._socket
|
|
106
|
+
if (!this._socket) {
|
|
64
107
|
throw new Error("Socket is not connected");
|
|
65
108
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
85
|
-
return this.subscribe(type, handler);
|
|
86
|
-
}
|
|
87
|
-
// Private methods
|
|
88
|
-
_handleMessage(rawData) {
|
|
121
|
+
_handleMessage(event, data) {
|
|
89
122
|
try {
|
|
90
|
-
const
|
|
91
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
131
|
-
return Boolean(
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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:
|
|
288
|
-
expiresIn:
|
|
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
|
/**
|