@symbo.ls/sdk 2.32.6 → 2.32.8
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/cjs/config/environment.js +19 -0
- package/dist/cjs/services/AuthService.js +4 -2
- package/dist/cjs/services/CollabService.js +214 -64
- package/dist/cjs/services/ProjectService.js +19 -10
- package/dist/cjs/utils/CollabClient.js +46 -9
- package/dist/esm/config/environment.js +19 -0
- package/dist/esm/index.js +302 -85
- package/dist/esm/services/AdminService.js +19 -0
- package/dist/esm/services/AuthService.js +23 -2
- package/dist/esm/services/BaseService.js +19 -0
- package/dist/esm/services/BranchService.js +19 -0
- package/dist/esm/services/CollabService.js +279 -73
- package/dist/esm/services/CoreService.js +19 -0
- package/dist/esm/services/DnsService.js +19 -0
- package/dist/esm/services/FileService.js +19 -0
- package/dist/esm/services/PaymentService.js +19 -0
- package/dist/esm/services/PlanService.js +19 -0
- package/dist/esm/services/ProjectService.js +38 -10
- package/dist/esm/services/PullRequestService.js +19 -0
- package/dist/esm/services/ScreenshotService.js +19 -0
- package/dist/esm/services/SubscriptionService.js +19 -0
- package/dist/esm/services/index.js +302 -85
- package/dist/esm/utils/CollabClient.js +65 -9
- package/dist/node/config/environment.js +19 -0
- package/dist/node/services/AuthService.js +4 -2
- package/dist/node/services/CollabService.js +214 -64
- package/dist/node/services/ProjectService.js +19 -10
- package/dist/node/utils/CollabClient.js +46 -9
- package/package.json +6 -6
- package/src/config/environment.js +20 -1
- package/src/services/AuthService.js +7 -2
- package/src/services/CollabService.js +258 -77
- package/src/services/ProjectService.js +19 -10
- package/src/utils/CollabClient.js +51 -8
|
@@ -15,8 +15,8 @@ class CollabClient {
|
|
|
15
15
|
_buffer = [];
|
|
16
16
|
_flushTimer = null;
|
|
17
17
|
_clientId = nanoid();
|
|
18
|
-
_outboxStore =
|
|
19
|
-
// Dexie table
|
|
18
|
+
_outboxStore = createMemoryOutbox();
|
|
19
|
+
// Dexie table fallback
|
|
20
20
|
_readyResolve;
|
|
21
21
|
ready = new Promise((res) => this._readyResolve = res);
|
|
22
22
|
constructor({ jwt, projectId, branch = "main", live = false }) {
|
|
@@ -32,14 +32,11 @@ class CollabClient {
|
|
|
32
32
|
console.warn("[CollabClient] Failed to load IndexeddbPersistence:", err);
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
-
if (typeof window
|
|
36
|
-
this._outboxStore = createMemoryOutbox();
|
|
37
|
-
} else {
|
|
35
|
+
if (typeof window !== "undefined" && hasIndexedDB) {
|
|
38
36
|
createDexieOutbox(`${projectId}:${branch}`).then((outboxStore) => {
|
|
39
37
|
this._outboxStore = outboxStore;
|
|
40
38
|
}).catch((err) => {
|
|
41
39
|
console.warn("[CollabClient] Failed to load Dexie:", err);
|
|
42
|
-
this._outboxStore = createMemoryOutbox();
|
|
43
40
|
});
|
|
44
41
|
}
|
|
45
42
|
this.socket = io(environment.socketUrl, {
|
|
@@ -49,9 +46,7 @@ class CollabClient {
|
|
|
49
46
|
reconnectionAttempts: Infinity,
|
|
50
47
|
reconnectionDelayMax: 4e3
|
|
51
48
|
});
|
|
52
|
-
this.socket.on("snapshot", this._onSnapshot).on("ops", this._onOps).on("commit", this._onCommit).on("liveMode", (
|
|
53
|
-
this.live = flag;
|
|
54
|
-
}).on("connect", this._onConnect).on("error", (e) => console.warn("[collab] socket error", e));
|
|
49
|
+
this.socket.on("snapshot", this._onSnapshot).on("ops", this._onOps).on("commit", this._onCommit).on("liveMode", this._onLiveMode).on("connect", this._onConnect).on("error", this._onError);
|
|
55
50
|
this._prevJson = this.ydoc.getMap("root").toJSON();
|
|
56
51
|
this.ydoc.on("afterTransaction", (tr) => {
|
|
57
52
|
if (tr.origin === "remote") {
|
|
@@ -137,6 +132,48 @@ class CollabClient {
|
|
|
137
132
|
});
|
|
138
133
|
this._buffer.length = 0;
|
|
139
134
|
}
|
|
135
|
+
dispose() {
|
|
136
|
+
var _a;
|
|
137
|
+
clearTimeout(this._flushTimer);
|
|
138
|
+
this._flushTimer = null;
|
|
139
|
+
this._buffer.length = 0;
|
|
140
|
+
if ((_a = this._outboxStore) == null ? void 0 : _a.clear) {
|
|
141
|
+
try {
|
|
142
|
+
const result = this._outboxStore.clear();
|
|
143
|
+
if (result && typeof result.catch === "function") {
|
|
144
|
+
result.catch(() => {
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.warn("[CollabClient] Failed to clear outbox store during dispose:", error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (this.socket) {
|
|
152
|
+
this.socket.off("snapshot", this._onSnapshot);
|
|
153
|
+
this.socket.off("ops", this._onOps);
|
|
154
|
+
this.socket.off("commit", this._onCommit);
|
|
155
|
+
this.socket.off("liveMode", this._onLiveMode);
|
|
156
|
+
this.socket.off("connect", this._onConnect);
|
|
157
|
+
this.socket.off("error", this._onError);
|
|
158
|
+
this.socket.removeAllListeners();
|
|
159
|
+
this.socket.disconnect();
|
|
160
|
+
this.socket = null;
|
|
161
|
+
}
|
|
162
|
+
if (this.ydoc) {
|
|
163
|
+
this.ydoc.destroy();
|
|
164
|
+
this.ydoc = null;
|
|
165
|
+
}
|
|
166
|
+
if (typeof this._readyResolve === "function") {
|
|
167
|
+
this._readyResolve();
|
|
168
|
+
this._readyResolve = null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
_onLiveMode = (flag) => {
|
|
172
|
+
this.live = flag;
|
|
173
|
+
};
|
|
174
|
+
_onError = (e) => {
|
|
175
|
+
console.warn("[collab] socket error", e);
|
|
176
|
+
};
|
|
140
177
|
}
|
|
141
178
|
function createMemoryOutbox() {
|
|
142
179
|
const store = /* @__PURE__ */ new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/sdk",
|
|
3
|
-
"version": "2.32.
|
|
3
|
+
"version": "2.32.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
"test:user": "cross-env NODE_ENV=$NODE_ENV npx tape integration-tests/index.js integration-tests/user/*.test.js | tap-spec"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@domql/element": "^2.32.
|
|
50
|
-
"@domql/utils": "^2.32.
|
|
51
|
-
"@symbo.ls/router": "^2.32.
|
|
52
|
-
"@symbo.ls/socket": "^2.32.
|
|
49
|
+
"@domql/element": "^2.32.8",
|
|
50
|
+
"@domql/utils": "^2.32.8",
|
|
51
|
+
"@symbo.ls/router": "^2.32.8",
|
|
52
|
+
"@symbo.ls/socket": "^2.32.8",
|
|
53
53
|
"acorn": "^8.14.0",
|
|
54
54
|
"acorn-walk": "^8.3.4",
|
|
55
55
|
"dexie": "^4.0.11",
|
|
@@ -71,5 +71,5 @@
|
|
|
71
71
|
"tap-spec": "^5.0.0",
|
|
72
72
|
"tape": "^5.9.0"
|
|
73
73
|
},
|
|
74
|
-
"gitHead": "
|
|
74
|
+
"gitHead": "f7531cb41b391a1357d31a03fbdd33ad93e0a29d"
|
|
75
75
|
}
|
|
@@ -24,6 +24,9 @@ const CONFIG = {
|
|
|
24
24
|
basedProject: 'platform-v2-sm', // For based api
|
|
25
25
|
basedOrg: 'symbols', // For based api
|
|
26
26
|
githubClientId: 'Ov23liAFrsR0StbAO6PO', // For github api
|
|
27
|
+
grafanaUrl:
|
|
28
|
+
'https://faro-collector-prod-us-east-0.grafana.net/collect/aef64330db80bdfeaac084317bf72f99', // For grafana tracing
|
|
29
|
+
grafanaAppName: 'Localhost Symbols',
|
|
27
30
|
// Environment-specific feature toggles (override common)
|
|
28
31
|
features: {
|
|
29
32
|
betaFeatures: true // Enable beta features in local dev
|
|
@@ -38,6 +41,9 @@ const CONFIG = {
|
|
|
38
41
|
socketUrl: 'https://dev.api.symbols.app',
|
|
39
42
|
apiUrl: 'https://dev.api.symbols.app',
|
|
40
43
|
githubClientId: 'Ov23liHxyWFBxS8f1gnF',
|
|
44
|
+
grafanaUrl:
|
|
45
|
+
'https://faro-collector-prod-us-east-0.grafana.net/collect/7a3ba473cee2025c68513667024316b8', // For grafana tracing
|
|
46
|
+
grafanaAppName: 'Symbols Dev',
|
|
41
47
|
typesenseCollectionName: 'docs',
|
|
42
48
|
typesenseApiKey: 'awmcVpbWqZi9IUgmvslp1C5LKDU8tMjA',
|
|
43
49
|
typesenseHost: 'tl2qpnwxev4cjm36p-1.a1.typesense.net',
|
|
@@ -51,6 +57,9 @@ const CONFIG = {
|
|
|
51
57
|
basedProject: 'platform-v2-sm',
|
|
52
58
|
basedOrg: 'symbols',
|
|
53
59
|
githubClientId: 'Ov23liHxyWFBxS8f1gnF',
|
|
60
|
+
grafanaUrl:
|
|
61
|
+
'https://faro-collector-prod-us-east-0.grafana.net/collect/7a3ba473cee2025c68513667024316b8', // For grafana tracing
|
|
62
|
+
grafanaAppName: 'Symbols Test',
|
|
54
63
|
typesenseCollectionName: 'docs',
|
|
55
64
|
typesenseApiKey: 'awmcVpbWqZi9IUgmvslp1C5LKDU8tMjA',
|
|
56
65
|
typesenseHost: 'tl2qpnwxev4cjm36p-1.a1.typesense.net',
|
|
@@ -61,6 +70,9 @@ const CONFIG = {
|
|
|
61
70
|
socketUrl: 'https://upcoming.api.symbols.app',
|
|
62
71
|
apiUrl: 'https://upcoming.api.symbols.app',
|
|
63
72
|
githubClientId: 'Ov23liWF7NvdZ056RV5J',
|
|
73
|
+
grafanaUrl:
|
|
74
|
+
'https://faro-collector-prod-us-east-0.grafana.net/collect/7a3ba473cee2025c68513667024316b8', // For grafana tracing
|
|
75
|
+
grafanaAppName: 'Symbols Upcoming',
|
|
64
76
|
typesenseCollectionName: 'docs',
|
|
65
77
|
typesenseApiKey: 'awmcVpbWqZi9IUgmvslp1C5LKDU8tMjA',
|
|
66
78
|
typesenseHost: 'tl2qpnwxev4cjm36p-1.a1.typesense.net',
|
|
@@ -74,6 +86,9 @@ const CONFIG = {
|
|
|
74
86
|
basedProject: 'platform-v2-sm',
|
|
75
87
|
basedOrg: 'symbols',
|
|
76
88
|
githubClientId: 'Ov23ligwZDQVD0VfuWNa',
|
|
89
|
+
grafanaUrl:
|
|
90
|
+
'https://faro-collector-prod-us-east-0.grafana.net/collect/7a3ba473cee2025c68513667024316b8', // For grafana tracing
|
|
91
|
+
grafanaAppName: 'Symbols Staging',
|
|
77
92
|
typesenseCollectionName: 'docs',
|
|
78
93
|
typesenseApiKey: 'awmcVpbWqZi9IUgmvslp1C5LKDU8tMjA',
|
|
79
94
|
typesenseHost: 'tl2qpnwxev4cjm36p-1.a1.typesense.net',
|
|
@@ -87,6 +102,9 @@ const CONFIG = {
|
|
|
87
102
|
basedProject: 'platform-v2-sm',
|
|
88
103
|
basedOrg: 'symbols',
|
|
89
104
|
githubClientId: 'Ov23liFAlOEIXtX3dBtR',
|
|
105
|
+
grafanaUrl:
|
|
106
|
+
'https://faro-collector-prod-us-east-0.grafana.net/collect/5c1089f3c3eea4ec5658e05c3f53baae', // For grafana tracing
|
|
107
|
+
grafanaAppName: 'Symbols',
|
|
90
108
|
typesenseCollectionName: 'docs',
|
|
91
109
|
typesenseApiKey: 'awmcVpbWqZi9IUgmvslp1C5LKDU8tMjA',
|
|
92
110
|
typesenseHost: 'tl2qpnwxev4cjm36p-1.a1.typesense.net',
|
|
@@ -125,6 +143,7 @@ export const getConfig = () => {
|
|
|
125
143
|
basedOrg: process.env.SYMBOLS_APP_BASED_ORG || envConfig.basedOrg,
|
|
126
144
|
githubClientId:
|
|
127
145
|
process.env.SYMBOLS_APP_GITHUB_CLIENT_ID || envConfig.githubClientId,
|
|
146
|
+
grafanaUrl: process.env.SYMBOLS_APP_GRAFANA_URL || envConfig.grafanaUrl,
|
|
128
147
|
typesenseCollectionName:
|
|
129
148
|
process.env.TYPESENSE_COLLECTION_NAME ||
|
|
130
149
|
envConfig.typesenseCollectionName,
|
|
@@ -149,7 +168,7 @@ export const getConfig = () => {
|
|
|
149
168
|
'googleClientId'
|
|
150
169
|
]
|
|
151
170
|
|
|
152
|
-
const missingFields = requiredFields.filter(
|
|
171
|
+
const missingFields = requiredFields.filter(field => !finalConfig[field])
|
|
153
172
|
|
|
154
173
|
if (missingFields.length > 0) {
|
|
155
174
|
console.error(
|
|
@@ -349,10 +349,15 @@ export class AuthService extends BaseService {
|
|
|
349
349
|
}
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
-
async getMe() {
|
|
352
|
+
async getMe(options = {}) {
|
|
353
353
|
this._requireReady('getMe')
|
|
354
354
|
try {
|
|
355
|
-
const
|
|
355
|
+
const session = this._resolvePluginSession(options.session)
|
|
356
|
+
const endpoint = session
|
|
357
|
+
? `/auth/me?session=${encodeURIComponent(session)}`
|
|
358
|
+
: '/auth/me'
|
|
359
|
+
|
|
360
|
+
const response = await this._request(endpoint, {
|
|
356
361
|
method: 'GET',
|
|
357
362
|
methodName: 'getMe'
|
|
358
363
|
})
|
|
@@ -12,15 +12,24 @@ export class CollabService extends BaseService {
|
|
|
12
12
|
this._client = null
|
|
13
13
|
this._stateManager = null
|
|
14
14
|
this._connected = false
|
|
15
|
+
this._connecting = false
|
|
16
|
+
this._connectPromise = null
|
|
17
|
+
this._connectionMeta = null
|
|
18
|
+
this._pendingConnectReject = null
|
|
15
19
|
this._undoStack = []
|
|
16
20
|
this._redoStack = []
|
|
17
21
|
this._isUndoRedo = false
|
|
18
22
|
// Store operations made while offline so they can be flushed once the
|
|
19
23
|
// socket reconnects.
|
|
20
24
|
this._pendingOps = []
|
|
25
|
+
|
|
26
|
+
this._onSocketConnect = this._onSocketConnect.bind(this)
|
|
27
|
+
this._onSocketDisconnect = this._onSocketDisconnect.bind(this)
|
|
28
|
+
this._onSocketError = this._onSocketError.bind(this)
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
init({ context }) {
|
|
32
|
+
super.init({ context })
|
|
24
33
|
// console.log('CollabService init')
|
|
25
34
|
// console.log(context)
|
|
26
35
|
|
|
@@ -105,109 +114,243 @@ export class CollabService extends BaseService {
|
|
|
105
114
|
|
|
106
115
|
/* ---------- Connection Management ---------- */
|
|
107
116
|
async connect(options = {}) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
this._ensureStateManager()
|
|
111
|
-
|
|
112
|
-
const {
|
|
113
|
-
authToken: jwt,
|
|
114
|
-
projectId,
|
|
115
|
-
branch = 'main',
|
|
116
|
-
pro
|
|
117
|
-
} = {
|
|
118
|
-
...this._context,
|
|
119
|
-
...options
|
|
117
|
+
if (this._connectPromise) {
|
|
118
|
+
return this._connectPromise
|
|
120
119
|
}
|
|
121
|
-
// console.log(jwt, projectId, branch, pro)
|
|
122
120
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
el.call('openNotification', {
|
|
127
|
-
type: 'error',
|
|
128
|
-
title: 'projectId is required',
|
|
129
|
-
message: 'projectId is required for CollabService connection'
|
|
130
|
-
})
|
|
131
|
-
throw new Error('projectId is required for CollabService connection')
|
|
132
|
-
}
|
|
121
|
+
this._connectPromise = (async () => {
|
|
122
|
+
this._connecting = true
|
|
123
|
+
this._connected = false
|
|
133
124
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
125
|
+
// Make sure we have the state manager ready now that the context should
|
|
126
|
+
// contain the root state (after updateSDKContext()).
|
|
127
|
+
this._ensureStateManager()
|
|
138
128
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
live: Boolean(pro)
|
|
144
|
-
})
|
|
129
|
+
const mergedOptions = {
|
|
130
|
+
...this._context,
|
|
131
|
+
...options
|
|
132
|
+
}
|
|
145
133
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
134
|
+
let { authToken: jwt } = mergedOptions
|
|
135
|
+
const {
|
|
136
|
+
projectId,
|
|
137
|
+
branch = 'main',
|
|
138
|
+
pro
|
|
139
|
+
} = mergedOptions
|
|
140
|
+
|
|
141
|
+
if (!jwt && this._tokenManager) {
|
|
142
|
+
try {
|
|
143
|
+
jwt = await this._tokenManager.ensureValidToken()
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.warn('[CollabService] Failed to obtain auth token from token manager', error)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!jwt && typeof this._tokenManager.getAccessToken === 'function') {
|
|
149
|
+
jwt = this._tokenManager.getAccessToken()
|
|
150
|
+
}
|
|
154
151
|
}
|
|
155
|
-
})
|
|
156
152
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
this._stateManager.applyChanges(changes, { fromSocket: true })
|
|
161
|
-
})
|
|
153
|
+
if (!jwt) {
|
|
154
|
+
throw new Error('[CollabService] Cannot connect without auth token')
|
|
155
|
+
}
|
|
162
156
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
157
|
+
this._context = {
|
|
158
|
+
...this._context,
|
|
159
|
+
authToken: jwt,
|
|
160
|
+
projectId,
|
|
161
|
+
branch,
|
|
162
|
+
pro
|
|
166
163
|
}
|
|
167
164
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
if (!projectId) {
|
|
166
|
+
const state = this._stateManager?.root
|
|
167
|
+
const el = state.__element
|
|
168
|
+
el.call('openNotification', {
|
|
169
|
+
type: 'error',
|
|
170
|
+
title: 'projectId is required',
|
|
171
|
+
message: 'projectId is required for CollabService connection'
|
|
172
|
+
})
|
|
173
|
+
throw new Error('projectId is required for CollabService connection')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Disconnect existing connection if any
|
|
177
|
+
if (this._client) {
|
|
178
|
+
await this.disconnect()
|
|
179
|
+
}
|
|
171
180
|
|
|
172
|
-
|
|
173
|
-
|
|
181
|
+
this._client = new CollabClient({
|
|
182
|
+
jwt,
|
|
183
|
+
projectId,
|
|
184
|
+
branch,
|
|
185
|
+
live: Boolean(pro)
|
|
186
|
+
})
|
|
174
187
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
this.
|
|
179
|
-
)
|
|
180
|
-
this._client.socket?.on(
|
|
181
|
-
'bundle:error',
|
|
182
|
-
this._handleBundleErrorEvent.bind(this)
|
|
183
|
-
)
|
|
188
|
+
// Mark as connected once the socket establishes a connection. This prevents
|
|
189
|
+
// the SDK from being stuck waiting for an initial snapshot that may never
|
|
190
|
+
// arrive (e.g. for new/empty documents).
|
|
191
|
+
const {socket} = this._client
|
|
184
192
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
193
|
+
try {
|
|
194
|
+
await new Promise((resolve, reject) => {
|
|
195
|
+
if (!socket) {
|
|
196
|
+
reject(new Error('[CollabService] Socket instance missing'))
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (socket.connected) {
|
|
201
|
+
resolve()
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* eslint-disable no-use-before-define */
|
|
206
|
+
const cleanup = () => {
|
|
207
|
+
socket.off('connect', handleConnect)
|
|
208
|
+
socket.off('connect_error', handleError)
|
|
209
|
+
socket.off('error', handleError)
|
|
210
|
+
socket.off('disconnect', handleDisconnect)
|
|
211
|
+
if (this._pendingConnectReject === handleError) {
|
|
212
|
+
this._pendingConnectReject = null
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const handleConnect = () => {
|
|
217
|
+
cleanup()
|
|
218
|
+
resolve()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const handleError = (error) => {
|
|
222
|
+
cleanup()
|
|
223
|
+
reject(
|
|
224
|
+
error instanceof Error
|
|
225
|
+
? error
|
|
226
|
+
: new Error(String(error || 'Unknown connection error'))
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const handleDisconnect = (reason) => {
|
|
231
|
+
handleError(
|
|
232
|
+
reason instanceof Error
|
|
233
|
+
? reason
|
|
234
|
+
: new Error(
|
|
235
|
+
`[CollabService] Socket disconnected before connect: ${reason || 'unknown'}`
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this._pendingConnectReject = handleError
|
|
241
|
+
|
|
242
|
+
socket.once('connect', handleConnect)
|
|
243
|
+
socket.once('connect_error', handleError)
|
|
244
|
+
socket.once('error', handleError)
|
|
245
|
+
socket.once('disconnect', handleDisconnect)
|
|
246
|
+
/* eslint-enable no-use-before-define */
|
|
247
|
+
})
|
|
248
|
+
} catch (error) {
|
|
249
|
+
socket?.disconnect()
|
|
250
|
+
this._client = null
|
|
251
|
+
this._connectionMeta = null
|
|
252
|
+
throw error
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this._attachSocketLifecycleListeners()
|
|
256
|
+
if (socket?.connected) {
|
|
257
|
+
this._onSocketConnect()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Set up event listeners
|
|
261
|
+
socket?.on('ops', ({ changes }) => {
|
|
262
|
+
console.log(`ops event`)
|
|
263
|
+
this._stateManager.applyChanges(changes, { fromSocket: true })
|
|
192
264
|
})
|
|
193
|
-
this._pendingOps.length = 0
|
|
194
|
-
}
|
|
195
265
|
|
|
196
|
-
|
|
197
|
-
|
|
266
|
+
socket?.on('commit', ({ version }) => {
|
|
267
|
+
if (version) {
|
|
268
|
+
this._stateManager.setVersion(version)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Inform UI about automatic commit
|
|
272
|
+
rootBus.emit('checkpoint:done', { version, origin: 'auto' })
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
// 🔄 Presence / members / cursor updates
|
|
276
|
+
socket?.on('clients', this._handleClientsEvent.bind(this))
|
|
277
|
+
|
|
278
|
+
// 🗜️ Bundle events – emitted by the dependency bundler service
|
|
279
|
+
socket?.on(
|
|
280
|
+
'bundle:done',
|
|
281
|
+
this._handleBundleDoneEvent.bind(this)
|
|
282
|
+
)
|
|
283
|
+
socket?.on(
|
|
284
|
+
'bundle:error',
|
|
285
|
+
this._handleBundleErrorEvent.bind(this)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
// Flush any operations that were queued while we were offline.
|
|
289
|
+
if (this._pendingOps.length) {
|
|
290
|
+
console.log(
|
|
291
|
+
`[CollabService] Flushing ${this._pendingOps.length} offline operation batch(es)`
|
|
292
|
+
)
|
|
293
|
+
this._pendingOps.forEach(({ changes, granularChanges, orders }) => {
|
|
294
|
+
this.socket.emit('ops', { changes, granularChanges, orders, ts: Date.now() })
|
|
295
|
+
})
|
|
296
|
+
this._pendingOps.length = 0
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
await this._client.ready
|
|
300
|
+
|
|
301
|
+
this._connectionMeta = {
|
|
302
|
+
projectId,
|
|
303
|
+
branch,
|
|
304
|
+
live: Boolean(pro)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return this.getConnectionInfo()
|
|
308
|
+
})()
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
return await this._connectPromise
|
|
312
|
+
} finally {
|
|
313
|
+
this._connecting = false
|
|
314
|
+
this._connectPromise = null
|
|
315
|
+
}
|
|
198
316
|
}
|
|
199
317
|
|
|
200
318
|
disconnect() {
|
|
201
319
|
if (this._client?.socket) {
|
|
202
|
-
this.
|
|
320
|
+
if (this._pendingConnectReject) {
|
|
321
|
+
this._pendingConnectReject(new Error('[CollabService] Connection attempt aborted'))
|
|
322
|
+
this._pendingConnectReject = null
|
|
323
|
+
}
|
|
324
|
+
this._detachSocketLifecycleListeners()
|
|
325
|
+
if (typeof this._client.dispose === 'function') {
|
|
326
|
+
this._client.dispose()
|
|
327
|
+
} else {
|
|
328
|
+
this._client.socket.disconnect()
|
|
329
|
+
}
|
|
203
330
|
}
|
|
204
331
|
this._client = null
|
|
205
332
|
this._connected = false
|
|
333
|
+
this._connecting = false
|
|
334
|
+
this._connectionMeta = null
|
|
335
|
+
this._pendingConnectReject = null
|
|
206
336
|
console.log('[CollabService] Disconnected')
|
|
207
337
|
}
|
|
208
338
|
|
|
209
339
|
isConnected() {
|
|
210
|
-
return this._connected && this._client?.socket?.connected
|
|
340
|
+
return Boolean(this._connected && this._client?.socket?.connected)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
getConnectionInfo() {
|
|
344
|
+
return {
|
|
345
|
+
connected: this.isConnected(),
|
|
346
|
+
connecting: this._connecting,
|
|
347
|
+
projectId: this._connectionMeta?.projectId ?? null,
|
|
348
|
+
branch: this._connectionMeta?.branch ?? null,
|
|
349
|
+
live: this._connectionMeta?.live ?? null,
|
|
350
|
+
pendingOps: this._pendingOps.length,
|
|
351
|
+
undoStackSize: this.getUndoStackSize(),
|
|
352
|
+
redoStackSize: this.getRedoStackSize()
|
|
353
|
+
}
|
|
211
354
|
}
|
|
212
355
|
|
|
213
356
|
/* convenient shortcuts */
|
|
@@ -660,6 +803,44 @@ export class CollabService extends BaseService {
|
|
|
660
803
|
}
|
|
661
804
|
|
|
662
805
|
/* ---------- Manual checkpoint ---------- */
|
|
806
|
+
_attachSocketLifecycleListeners() {
|
|
807
|
+
const socket = this._client?.socket
|
|
808
|
+
if (!socket) {
|
|
809
|
+
return
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
socket.on('connect', this._onSocketConnect)
|
|
813
|
+
socket.on('disconnect', this._onSocketDisconnect)
|
|
814
|
+
socket.on('connect_error', this._onSocketError)
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
_detachSocketLifecycleListeners() {
|
|
818
|
+
const socket = this._client?.socket
|
|
819
|
+
if (!socket) {
|
|
820
|
+
return
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
socket.off('connect', this._onSocketConnect)
|
|
824
|
+
socket.off('disconnect', this._onSocketDisconnect)
|
|
825
|
+
socket.off('connect_error', this._onSocketError)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
_onSocketConnect() {
|
|
829
|
+
this._connected = true
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
_onSocketDisconnect(reason) {
|
|
833
|
+
this._connected = false
|
|
834
|
+
|
|
835
|
+
if (reason && reason !== 'io client disconnect') {
|
|
836
|
+
console.warn('[CollabService] Socket disconnected', reason)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
_onSocketError(error) {
|
|
841
|
+
console.warn('[CollabService] Socket connection error', error)
|
|
842
|
+
}
|
|
843
|
+
|
|
663
844
|
/**
|
|
664
845
|
* Manually request a checkpoint / commit of buffered operations on the server.
|
|
665
846
|
* Resolves with the new version number once the backend confirms via the
|