@symbo.ls/sync 3.14.9 → 3.14.11
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/CHANGELOG.md +26 -0
- package/SyncNotifications.js +120 -0
- package/applier.js +71 -0
- package/handlers.js +83 -0
- package/index.js +258 -0
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @symbo.ls/sync
|
|
2
2
|
|
|
3
|
+
## 3.14.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Manual patch bump triggered via workflow_dispatch (scope: @symbo.ls).
|
|
8
|
+
No source change behind this bump — released to refresh dist or
|
|
9
|
+
coordinate a cross-package version line.
|
|
10
|
+
- Updated dependencies
|
|
11
|
+
- @symbo.ls/default-config@3.14.15
|
|
12
|
+
- @symbo.ls/router@3.14.10
|
|
13
|
+
- @symbo.ls/scratch@3.14.13
|
|
14
|
+
- @symbo.ls/utils@3.14.15
|
|
15
|
+
|
|
16
|
+
## 3.14.10
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Manual patch bump triggered via workflow_dispatch (scope: @symbo.ls).
|
|
21
|
+
No source change behind this bump — released to refresh dist or
|
|
22
|
+
coordinate a cross-package version line.
|
|
23
|
+
- Updated dependencies
|
|
24
|
+
- @symbo.ls/default-config@3.14.12
|
|
25
|
+
- @symbo.ls/router@3.14.9
|
|
26
|
+
- @symbo.ls/scratch@3.14.10
|
|
27
|
+
- @symbo.ls/utils@3.14.11
|
|
28
|
+
|
|
3
29
|
## 3.14.9
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// const ANIMATION = {
|
|
4
|
+
// fadeInUp: {
|
|
5
|
+
// from: {
|
|
6
|
+
// transform: 'translate3d(0, 12.5%, 1px)',
|
|
7
|
+
// opacity: 0
|
|
8
|
+
// },
|
|
9
|
+
// to: {
|
|
10
|
+
// transform: 'translate3d(0, 0, 1px)',
|
|
11
|
+
// opacity: 1
|
|
12
|
+
// }
|
|
13
|
+
// },
|
|
14
|
+
// fadeOutDown: {
|
|
15
|
+
// from: {
|
|
16
|
+
// transform: 'translate3d(0, 0, 1px)',
|
|
17
|
+
// opacity: 1
|
|
18
|
+
// },
|
|
19
|
+
// to: {
|
|
20
|
+
// transform: 'translate3d(0, 12.5%, 1px)',
|
|
21
|
+
// opacity: 0
|
|
22
|
+
// }
|
|
23
|
+
// }
|
|
24
|
+
// }
|
|
25
|
+
|
|
26
|
+
// const COLOR = {
|
|
27
|
+
// black: '#000000',
|
|
28
|
+
// blue: '#3686F7'
|
|
29
|
+
// }
|
|
30
|
+
|
|
31
|
+
// set({
|
|
32
|
+
// COLOR,
|
|
33
|
+
// ANIMATION
|
|
34
|
+
// })
|
|
35
|
+
|
|
36
|
+
const NOTIF_COLORS = {
|
|
37
|
+
success: 'green',
|
|
38
|
+
error: 'red',
|
|
39
|
+
warning: 'yellow'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const connectedToSymbols = (clients, element, state) => {
|
|
43
|
+
if (clients.symbols) {
|
|
44
|
+
if (!state.connected) {
|
|
45
|
+
state.notifications.connected = {
|
|
46
|
+
title: 'Connected',
|
|
47
|
+
message: 'to the Symbols live server'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
state.update({ connected: true })
|
|
51
|
+
|
|
52
|
+
const t = setTimeout(() => {
|
|
53
|
+
delete state.notifications.connected
|
|
54
|
+
element.notifications.content.connected.update({
|
|
55
|
+
animation: 'fadeOutDown'
|
|
56
|
+
})
|
|
57
|
+
state.update({ connected: true })
|
|
58
|
+
clearTimeout(t)
|
|
59
|
+
}, 3000)
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
if (state.connected) {
|
|
63
|
+
state.notifications.connected = {
|
|
64
|
+
title: 'Disconnected',
|
|
65
|
+
type: 'error'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
state.update({ connected: false })
|
|
69
|
+
|
|
70
|
+
const t = setTimeout(() => {
|
|
71
|
+
delete state.notifications.connected
|
|
72
|
+
if (element.notifications.content.connected) {
|
|
73
|
+
element.notifications.content.connected.update({
|
|
74
|
+
animation: 'fadeOutDown'
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
state.update({ connected: true })
|
|
78
|
+
clearTimeout(t)
|
|
79
|
+
}, 3000)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const Notifications = {
|
|
85
|
+
state: {
|
|
86
|
+
notifications: []
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
Notifications: {
|
|
90
|
+
position: 'fixed',
|
|
91
|
+
left: 'A2',
|
|
92
|
+
bottom: 'Z2',
|
|
93
|
+
zIndex: '999',
|
|
94
|
+
childExtends: 'Notification',
|
|
95
|
+
childProps: ({ state }) => ({
|
|
96
|
+
animationDuration: 'C',
|
|
97
|
+
background: NOTIF_COLORS[state.type || 'success'],
|
|
98
|
+
icon: null,
|
|
99
|
+
Flex: {
|
|
100
|
+
Title: {
|
|
101
|
+
text: '{{ title }}'
|
|
102
|
+
},
|
|
103
|
+
P: {
|
|
104
|
+
text: '{{ title }}'
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
onRender: (e, el, s) => {
|
|
108
|
+
el.update({ animation: 'fadeInUp' })
|
|
109
|
+
},
|
|
110
|
+
onClick: (e, el, s) => {
|
|
111
|
+
delete s.notifications[el.key]
|
|
112
|
+
el.update({ animation: 'fadeOutDown' })
|
|
113
|
+
if (s.onClose) s.onClose(e, el, s)
|
|
114
|
+
}
|
|
115
|
+
}),
|
|
116
|
+
IconText: null,
|
|
117
|
+
childrenAs: 'state',
|
|
118
|
+
children: ({ state }) => state.notifications
|
|
119
|
+
}
|
|
120
|
+
}
|
package/applier.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Pure ops applier — Node-safe, zero DOM coupling. Both browser handlers
|
|
2
|
+
// (onSnapshot/onOps in `./handlers.js`) and server-side test code import
|
|
3
|
+
// from here. Anything DOM-touching (init(designSystem), s.update(...)
|
|
4
|
+
// re-renders, el.call('router', …)) lives in handlers.js.
|
|
5
|
+
|
|
6
|
+
// Forbidden path segments — block prototype-pollution writes coming over
|
|
7
|
+
// any sync transport. Without the filter, a remote
|
|
8
|
+
// `setPath(ctx, ['__proto__', 'isAdmin'], true)` would pollute
|
|
9
|
+
// Object.prototype across the whole runtime.
|
|
10
|
+
export const FORBIDDEN_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype'])
|
|
11
|
+
|
|
12
|
+
export const isSafePath = (path) => {
|
|
13
|
+
if (!Array.isArray(path)) return false
|
|
14
|
+
for (let i = 0; i < path.length; i++) {
|
|
15
|
+
if (FORBIDDEN_SEGMENTS.has(path[i])) return false
|
|
16
|
+
}
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const deletePath = (obj, path) => {
|
|
21
|
+
if (!obj || !isSafePath(path)) return
|
|
22
|
+
path.reduce((acc, v, i, arr) => {
|
|
23
|
+
if (acc && v in acc) {
|
|
24
|
+
if (i !== arr.length - 1) return acc[v]
|
|
25
|
+
delete acc[v]
|
|
26
|
+
}
|
|
27
|
+
return void 0
|
|
28
|
+
}, obj)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const setPath = (obj, path, value, createNestedObjects = false) => {
|
|
32
|
+
if (!obj || !isSafePath(path)) return
|
|
33
|
+
path.reduce((acc, v, i, arr) => {
|
|
34
|
+
if (!acc) return void 0
|
|
35
|
+
if (i !== arr.length - 1) {
|
|
36
|
+
if (!acc[v] && createNestedObjects) acc[v] = {}
|
|
37
|
+
return acc[v]
|
|
38
|
+
}
|
|
39
|
+
acc[v] = value
|
|
40
|
+
return void 0
|
|
41
|
+
}, obj)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Apply a batch of `[action, path, value]` ops to a context object.
|
|
46
|
+
* Returns the set of top-level keys that changed so the caller can decide
|
|
47
|
+
* which downstream re-render side-effects to run.
|
|
48
|
+
*
|
|
49
|
+
* Supported actions: 'set', 'update' (alias for set), 'delete'.
|
|
50
|
+
* Empty paths are skipped — replace at the root needs a snapshot, not an op.
|
|
51
|
+
*/
|
|
52
|
+
export const applyOpsToCtx = (ctx, changes) => {
|
|
53
|
+
const topLevelChanged = new Set()
|
|
54
|
+
if (!Array.isArray(changes)) return topLevelChanged
|
|
55
|
+
for (const [action, path, change] of changes) {
|
|
56
|
+
if (!Array.isArray(path) || !path.length) continue
|
|
57
|
+
topLevelChanged.add(path[0])
|
|
58
|
+
switch (action) {
|
|
59
|
+
case 'delete':
|
|
60
|
+
deletePath(ctx, path)
|
|
61
|
+
break
|
|
62
|
+
case 'update':
|
|
63
|
+
case 'set':
|
|
64
|
+
setPath(ctx, path, change, true)
|
|
65
|
+
break
|
|
66
|
+
default:
|
|
67
|
+
break
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return topLevelChanged
|
|
71
|
+
}
|
package/handlers.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Browser-only sync handlers. Imports `init` from smbls (which transitively
|
|
2
|
+
// loads scratch and touches `document`), so this module must not be imported
|
|
3
|
+
// from server-side code paths. Server-side tests/utilities should import from
|
|
4
|
+
// `./applier.js` instead — that's the pure ops layer.
|
|
5
|
+
|
|
6
|
+
import { init } from 'smbls'
|
|
7
|
+
import { overwriteShallow } from '@symbo.ls/utils'
|
|
8
|
+
import { applyOpsToCtx } from './applier.js'
|
|
9
|
+
|
|
10
|
+
export const onSnapshot =
|
|
11
|
+
(el, s, ctx) =>
|
|
12
|
+
(payload = {}) => {
|
|
13
|
+
let { data } = payload
|
|
14
|
+
const { schema } = payload
|
|
15
|
+
if (!data) return
|
|
16
|
+
|
|
17
|
+
data = el.call(
|
|
18
|
+
'deepDestringifyFunctions',
|
|
19
|
+
data,
|
|
20
|
+
Array.isArray(data) ? [] : {}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
Object.entries(data).forEach(([key, val]) => {
|
|
24
|
+
if (ctx[key] && typeof ctx[key] === 'object') {
|
|
25
|
+
if (key === 'designSystem') {
|
|
26
|
+
init(val)
|
|
27
|
+
} else {
|
|
28
|
+
overwriteShallow(ctx[key], val)
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
ctx[key] = val
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if (schema) ctx.schema = schema
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const onOps =
|
|
39
|
+
(el, s, ctx) =>
|
|
40
|
+
(payload = {}) => {
|
|
41
|
+
let { changes } = payload
|
|
42
|
+
if (!changes || !Array.isArray(changes) || !changes.length) return
|
|
43
|
+
|
|
44
|
+
changes = el.call(
|
|
45
|
+
'deepDestringifyFunctions',
|
|
46
|
+
changes,
|
|
47
|
+
Array.isArray(changes) ? [] : {}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const changed = applyOpsToCtx(ctx, changes)
|
|
51
|
+
|
|
52
|
+
// State changes → re-run state. Editor-driven navigation now goes
|
|
53
|
+
// through explicit `el.router(route)` calls (or a dedicated `navigate`
|
|
54
|
+
// event) instead of writing `state.route` as a side-effect channel.
|
|
55
|
+
if (changed.has('state')) {
|
|
56
|
+
s.update(ctx.state)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Source-section changes → soft reload at the current location so the
|
|
60
|
+
// new component bodies are picked up without a full page refresh.
|
|
61
|
+
// `assets` triggers the same rerender so updated variant URLs (e.g.
|
|
62
|
+
// a `?v=<mtime>` bump on a replaced image) propagate into the rendered
|
|
63
|
+
// <img>/<source> tags — without this, byte-edits to assets would
|
|
64
|
+
// refresh the manifest in ctx but never re-render the DOM.
|
|
65
|
+
if (
|
|
66
|
+
['pages', 'components', 'snippets', 'functions', 'assets'].some((k) =>
|
|
67
|
+
changed.has(k)
|
|
68
|
+
)
|
|
69
|
+
) {
|
|
70
|
+
const { pathname, search, hash } = ctx.window.location
|
|
71
|
+
el.call(
|
|
72
|
+
'router',
|
|
73
|
+
pathname + search + hash,
|
|
74
|
+
el.__ref.root,
|
|
75
|
+
{},
|
|
76
|
+
{ scrollToTop: false }
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (changed.has('designSystem')) {
|
|
81
|
+
init(ctx.designSystem)
|
|
82
|
+
}
|
|
83
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { router } from '@symbo.ls/router'
|
|
2
|
+
import { io } from 'socket.io-client'
|
|
3
|
+
import { window } from '@symbo.ls/utils'
|
|
4
|
+
import {
|
|
5
|
+
FORBIDDEN_SEGMENTS,
|
|
6
|
+
isSafePath,
|
|
7
|
+
deletePath,
|
|
8
|
+
setPath,
|
|
9
|
+
applyOpsToCtx
|
|
10
|
+
} from './applier.js'
|
|
11
|
+
import { onSnapshot, onOps } from './handlers.js'
|
|
12
|
+
import { connectedToSymbols, Notifications } from './SyncNotifications.js'
|
|
13
|
+
export { Notifications }
|
|
14
|
+
export {
|
|
15
|
+
FORBIDDEN_SEGMENTS,
|
|
16
|
+
isSafePath,
|
|
17
|
+
deletePath,
|
|
18
|
+
setPath,
|
|
19
|
+
applyOpsToCtx,
|
|
20
|
+
onSnapshot,
|
|
21
|
+
onOps
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isLocal = process.env.NODE_ENV === 'local'
|
|
25
|
+
|
|
26
|
+
const defaultBaseUrl = () =>
|
|
27
|
+
isLocal ? 'http://localhost:8080' : 'https://api.symbols.app'
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
* Detect which transport this page should speak. Two modes:
|
|
31
|
+
*
|
|
32
|
+
* collab — page was rendered by mermaid (hosted dev/staging/prod
|
|
33
|
+
* deploys). `ctx.editor.socketUrl` is injected at render time and
|
|
34
|
+
* points at the per-channel realtime API. Auth is a service token
|
|
35
|
+
* fetched from `${socketUrl}/service-token`; room is keyed by
|
|
36
|
+
* `ctx.key` (project key, or `editor.projectId` when present).
|
|
37
|
+
*
|
|
38
|
+
* runner — page was rendered by `@symbo.ls/runner` locally. The
|
|
39
|
+
* runner's HTTP server exposes a same-origin socket.io endpoint at
|
|
40
|
+
* the default `/socket.io` path with no auth. Detection:
|
|
41
|
+
* `ctx.editor.runtime === 'runner'` — the runner's boot script
|
|
42
|
+
* stamps that onto `context.editor` before calling `Smbls.create`.
|
|
43
|
+
* No window globals.
|
|
44
|
+
*
|
|
45
|
+
* Both modes attach the same applier handlers (onSnapshot/onOps) so the
|
|
46
|
+
* server-side wire format is interchangeable. Production builds (no
|
|
47
|
+
* socketUrl, no runner runtime) skip sync entirely.
|
|
48
|
+
*/
|
|
49
|
+
const detectTransport = (ctx) => {
|
|
50
|
+
const socketUrl =
|
|
51
|
+
(ctx && ctx.editor && ctx.editor.socketUrl) ||
|
|
52
|
+
(ctx && ctx.settings && ctx.settings.socketUrl)
|
|
53
|
+
if (typeof socketUrl === 'string' && socketUrl) {
|
|
54
|
+
return { mode: 'collab', baseUrl: socketUrl }
|
|
55
|
+
}
|
|
56
|
+
if (ctx && ctx.editor && ctx.editor.runtime === 'runner') {
|
|
57
|
+
return { mode: 'runner', baseUrl: null }
|
|
58
|
+
}
|
|
59
|
+
return { mode: null, baseUrl: null }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const fetchServiceToken = async (baseUrl) => {
|
|
63
|
+
try {
|
|
64
|
+
const urlBase = baseUrl || defaultBaseUrl()
|
|
65
|
+
const res = await window.fetch(`${urlBase}/service-token`, {
|
|
66
|
+
method: 'GET'
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Attempt to parse JSON first – recent versions return `{ token: "..." }`
|
|
70
|
+
// Fall back to treating the response as raw text for backward-compatibility.
|
|
71
|
+
let txt
|
|
72
|
+
try {
|
|
73
|
+
const json = await res.clone().json()
|
|
74
|
+
if (json && typeof json.token === 'string') {
|
|
75
|
+
return json.token.trim()
|
|
76
|
+
}
|
|
77
|
+
// If json parsing succeeds but no token field, fall back to text below.
|
|
78
|
+
txt = await res.text()
|
|
79
|
+
} catch {
|
|
80
|
+
// Response is not JSON – treat as plain text token.
|
|
81
|
+
txt = await res.text()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (txt || '').replace(/\s+/gu, '') || void 0
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.error('[sync] Failed to fetch service-token', e)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Wire the shared applier handlers onto a connected socket. Used by both
|
|
92
|
+
* collab and runner transports so the snapshot/ops protocol is uniform.
|
|
93
|
+
*/
|
|
94
|
+
const attachApplier = (socket, el, s, ctx) => {
|
|
95
|
+
socket.on('snapshot', onSnapshot(el, s, ctx))
|
|
96
|
+
socket.on('ops', onOps(el, s, ctx))
|
|
97
|
+
socket.on('clients', (data) => {
|
|
98
|
+
if (ctx.editor?.verbose) connectedToSymbols(data, el, s)
|
|
99
|
+
})
|
|
100
|
+
socket.on('disconnect', (reason) => {
|
|
101
|
+
if (ctx.editor?.verbose) console.info('[sync] disconnected', reason)
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const connectCollab = async (el, s, ctx, baseUrl) => {
|
|
106
|
+
const token = await fetchServiceToken(baseUrl)
|
|
107
|
+
if (!token) {
|
|
108
|
+
console.warn('[sync] no service token — collab disabled')
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
const projectKey = ctx.key
|
|
112
|
+
if (!projectKey) {
|
|
113
|
+
console.warn('[sync] ctx.key missing — collab cannot authenticate')
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
// The server prefers `projectId` (mongo _id) over `projectKey` lookup;
|
|
117
|
+
// mermaid injects it via `editor.projectId` to skip a 404-prone roundtrip.
|
|
118
|
+
const projectId = ctx.editor?.projectId || null
|
|
119
|
+
|
|
120
|
+
const socket = io(baseUrl, {
|
|
121
|
+
path: '/collab-socket',
|
|
122
|
+
transports: ['websocket'],
|
|
123
|
+
auth: {
|
|
124
|
+
token,
|
|
125
|
+
projectId,
|
|
126
|
+
projectKey,
|
|
127
|
+
branch: 'main',
|
|
128
|
+
live: true,
|
|
129
|
+
clientType: 'platform'
|
|
130
|
+
},
|
|
131
|
+
reconnectionAttempts: Infinity,
|
|
132
|
+
reconnectionDelayMax: 4000
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
socket.on('connect', () => {
|
|
136
|
+
if (ctx.editor?.verbose) console.info('[sync] connected (collab)')
|
|
137
|
+
})
|
|
138
|
+
attachApplier(socket, el, s, ctx)
|
|
139
|
+
return socket
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const connectRunner = (el, s, ctx) => {
|
|
143
|
+
// Same-origin /socket.io (default path), no auth — the runner is a
|
|
144
|
+
// local-only dev surface. Auto-reconnect so a server restart doesn't
|
|
145
|
+
// require a browser refresh.
|
|
146
|
+
const socket = io({
|
|
147
|
+
transports: ['websocket', 'polling'],
|
|
148
|
+
reconnectionAttempts: Infinity,
|
|
149
|
+
reconnectionDelayMax: 4000
|
|
150
|
+
})
|
|
151
|
+
socket.on('connect', () => {
|
|
152
|
+
if (ctx.editor?.verbose !== false) console.info('[sync] connected (runner)')
|
|
153
|
+
})
|
|
154
|
+
attachApplier(socket, el, s, ctx)
|
|
155
|
+
// Runner emits `reload` when something the wire format can't express
|
|
156
|
+
// changes (importmap shift, add/remove of dependencies). The browser
|
|
157
|
+
// must do a full reload to pick up the new HTML scaffold.
|
|
158
|
+
socket.on('reload', () => {
|
|
159
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
160
|
+
window.location.reload()
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
return socket
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const connectToSocket = async (el, s, ctx) => {
|
|
167
|
+
const { mode, baseUrl } = detectTransport(ctx)
|
|
168
|
+
if (!mode) return null
|
|
169
|
+
if (mode === 'collab') return connectCollab(el, s, ctx, baseUrl)
|
|
170
|
+
if (mode === 'runner') return connectRunner(el, s, ctx)
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// `onInitSync` was a custom lifecycle name that v3.14 never actually fires —
|
|
175
|
+
// `initSync` is registered in LIFECYCLE_EVENTS but no code triggers it, so the
|
|
176
|
+
// handler stayed dead and the realtime socket never opened on deployed pages.
|
|
177
|
+
// `onInit` is a fired root-element lifecycle: when SyncComponent is pushed
|
|
178
|
+
// into the app's `extends` (only on the root by initializeSync), this runs
|
|
179
|
+
// once at app boot and connects to the collab/mermaid backend.
|
|
180
|
+
export const SyncComponent = {
|
|
181
|
+
onInit: connectToSocket
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// `Inspect` moved to `@symbo.ls/inspect`. Compose it manually if you want
|
|
185
|
+
// the legacy DefaultSyncApp shape:
|
|
186
|
+
// import { Inspect } from '@symbo.ls/inspect'
|
|
187
|
+
// const DefaultApp = { extends: [SyncComponent, Inspect, Notifications] }
|
|
188
|
+
export const DefaultSyncApp = {
|
|
189
|
+
extends: [SyncComponent, Notifications]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const isUndef = (v) => typeof v === 'undefined'
|
|
193
|
+
const hasCollab = (ctx) => !!(ctx?.editor?.socketUrl || ctx?.settings?.socketUrl)
|
|
194
|
+
const hasRunner = (ctx) => ctx?.editor?.runtime === 'runner'
|
|
195
|
+
|
|
196
|
+
const attachExtend = (app, component) => {
|
|
197
|
+
if (Array.isArray(app.extends)) app.extends.push(component)
|
|
198
|
+
else if (app.extends) app.extends = [app.extends, component]
|
|
199
|
+
else app.extends = [component]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @symbo.ls/sync as a regular smbls plugin. Auto-registered by
|
|
204
|
+
* `prepareContext` in `packages/smbls/src/createDomql.js` (default-on);
|
|
205
|
+
* users opt out by setting `context.sync = false` or by removing it from
|
|
206
|
+
* `context.plugins` before calling `Smbls.create(...)`.
|
|
207
|
+
*
|
|
208
|
+
* The `beforeCreate(app, ctx)` lifecycle runs after `initializeExtend`
|
|
209
|
+
* has set the root `app.extends` array but before DOMQL element creation,
|
|
210
|
+
* so attaching SyncComponent here causes its `onInit` to fire as a normal
|
|
211
|
+
* root-element lifecycle event against the resolved app — no window
|
|
212
|
+
* globals, no Promise race.
|
|
213
|
+
*/
|
|
214
|
+
export const syncPlugin = {
|
|
215
|
+
name: 'sync',
|
|
216
|
+
beforeCreate (app, ctx) {
|
|
217
|
+
const editor = ctx && ctx.editor
|
|
218
|
+
|
|
219
|
+
// Precedence:
|
|
220
|
+
// explicit editor.liveSync → wins
|
|
221
|
+
// else: editor.socketUrl (collab) OR editor.runtime === 'runner'
|
|
222
|
+
// else: off (production / static dist)
|
|
223
|
+
const liveSync = editor && !isUndef(editor.liveSync)
|
|
224
|
+
? editor.liveSync
|
|
225
|
+
: (hasCollab(ctx) || hasRunner(ctx))
|
|
226
|
+
if (!liveSync) return
|
|
227
|
+
|
|
228
|
+
// Collab transport requires ctx.key for socket auth. Runner doesn't.
|
|
229
|
+
if (hasCollab(ctx) && !hasRunner(ctx) && !ctx?.key) return
|
|
230
|
+
|
|
231
|
+
attachExtend(app, SyncComponent)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Notifications overlay (toast stack at bottom-left). Visualises sync
|
|
237
|
+
* connect/disconnect events surfaced by `connectedToSymbols`. Lives in
|
|
238
|
+
* `@symbo.ls/sync` rather than its own package because the helper that
|
|
239
|
+
* fires notifications is owned by sync's collab transport.
|
|
240
|
+
*
|
|
241
|
+
* Auto-registered by `prepareContext` only when the user opts in
|
|
242
|
+
* (default OFF). Two opt-in shapes for backwards compatibility:
|
|
243
|
+
* 1. `context.notifications = true` (preferred — same shape as
|
|
244
|
+
* `context.sync` / `context.inspect`)
|
|
245
|
+
* 2. `context.editor.verbose = true` (legacy — historic behaviour
|
|
246
|
+
* where auto-on-in-dev also turned this on)
|
|
247
|
+
*/
|
|
248
|
+
export const notificationsPlugin = {
|
|
249
|
+
name: 'notifications',
|
|
250
|
+
beforeCreate (app, ctx) {
|
|
251
|
+
if (!ctx) return
|
|
252
|
+
const explicit = !isUndef(ctx.notifications) ? ctx.notifications : null
|
|
253
|
+
const editorVerbose = ctx.editor && ctx.editor.verbose === true
|
|
254
|
+
const enabled = explicit === true || (explicit !== false && editorVerbose)
|
|
255
|
+
if (!enabled) return
|
|
256
|
+
attachExtend(app, Notifications)
|
|
257
|
+
}
|
|
258
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/sync",
|
|
3
|
-
"version": "3.14.
|
|
3
|
+
"version": "3.14.11",
|
|
4
4
|
"main": "./dist/cjs/index.js",
|
|
5
5
|
"module": "./dist/esm/index.js",
|
|
6
6
|
"gitHead": "9fc1b79b41cdc725ca6b24aec64920a599634681",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist",
|
|
9
|
+
"*.js",
|
|
9
10
|
"*.md",
|
|
10
11
|
"LICENSE"
|
|
11
12
|
],
|
|
@@ -44,10 +45,10 @@
|
|
|
44
45
|
"prepublishOnly": "npm run build"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
|
-
"@symbo.ls/router": "^3.14.
|
|
48
|
-
"@symbo.ls/utils": "^3.14.
|
|
49
|
-
"@symbo.ls/scratch": "^3.14.
|
|
50
|
-
"@symbo.ls/default-config": "^3.14.
|
|
48
|
+
"@symbo.ls/router": "^3.14.10",
|
|
49
|
+
"@symbo.ls/utils": "^3.14.15",
|
|
50
|
+
"@symbo.ls/scratch": "^3.14.13",
|
|
51
|
+
"@symbo.ls/default-config": "^3.14.15",
|
|
51
52
|
"chalk": "^5.4.1",
|
|
52
53
|
"socket.io-client": "^4.8.1"
|
|
53
54
|
},
|