@symbo.ls/sync 3.14.1 → 3.14.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/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # @symbo.ls/sync
2
+
3
+ ## 3.14.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Auto-generated cross-repo patch release.
8
+ - Updated dependencies
9
+ - @symbo.ls/default-config@3.14.4
10
+
11
+ ## 3.14.1
12
+
13
+ ### Patch Changes
14
+
15
+ - Auto-generated patch release for changed packages.
16
+ - Updated dependencies
17
+ - @symbo.ls/default-config@3.14.2
18
+ - @symbo.ls/scratch@3.14.2
19
+ - @symbo.ls/utils@3.14.2
20
+ - @symbo.ls/router@3.14.1
@@ -0,0 +1 @@
1
+ "use strict";var c=Object.defineProperty;var d=Object.getOwnPropertyDescriptor;var a=Object.getOwnPropertyNames;var s=Object.prototype.hasOwnProperty;var l=(t,e)=>{for(var n in e)c(t,n,{get:e[n],enumerable:!0})},f=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of a(e))!s.call(t,i)&&i!==n&&c(t,i,{get:()=>e[i],enumerable:!(o=d(e,i))||o.enumerable});return t};var r=t=>f(c({},"__esModule",{value:!0}),t);var x={};l(x,{Notifications:()=>m,connectedToSymbols:()=>p});module.exports=r(x);const u={success:"green",error:"red",warning:"yellow"},p=(t,e,n)=>{if(t.symbols){if(!n.connected){n.notifications.connected={title:"Connected",message:"to the Symbols live server"},n.update({connected:!0});const o=setTimeout(()=>{delete n.notifications.connected,e.notifications.content.connected.update({animation:"fadeOutDown"}),n.update({connected:!0}),clearTimeout(o)},3e3)}}else if(n.connected){n.notifications.connected={title:"Disconnected",type:"error"},n.update({connected:!1});const o=setTimeout(()=>{delete n.notifications.connected,e.notifications.content.connected&&e.notifications.content.connected.update({animation:"fadeOutDown"}),n.update({connected:!0}),clearTimeout(o)},3e3)}},m={state:{notifications:[]},Notifications:{position:"fixed",left:"A2",bottom:"Z2",zIndex:"999",childExtends:"Notification",childProps:({state:t})=>({animationDuration:"C",background:u[t.type||"success"],icon:null,Flex:{Title:{text:"{{ title }}"},P:{text:"{{ title }}"}},onRender:(e,n,o)=>{n.update({animation:"fadeInUp"})},onClick:(e,n,o)=>{delete o.notifications[n.key],n.update({animation:"fadeOutDown"}),o.onClose&&o.onClose(e,n,o)}}),IconText:null,childrenAs:"state",children:({state:t})=>t.notifications}};
@@ -0,0 +1 @@
1
+ var s=Object.defineProperty;var y=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var A=Object.prototype.hasOwnProperty;var g=(r,e)=>{for(var n in e)s(r,n,{get:e[n],enumerable:!0})},x=(r,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of h(e))!A.call(r,t)&&t!==n&&s(r,t,{get:()=>e[t],enumerable:!(o=y(e,t))||o.enumerable});return r};var S=r=>x(s({},"__esModule",{value:!0}),r);var k={};g(k,{FORBIDDEN_SEGMENTS:()=>f,applyOpsToCtx:()=>_,deletePath:()=>a,isSafePath:()=>u,setPath:()=>l});module.exports=S(k);const f=new Set(["__proto__","constructor","prototype"]),u=r=>{if(!Array.isArray(r))return!1;for(let e=0;e<r.length;e++)if(f.has(r[e]))return!1;return!0},a=(r,e)=>{!r||!u(e)||e.reduce((n,o,t,i)=>{if(n&&o in n){if(t!==i.length-1)return n[o];delete n[o]}},r)},l=(r,e,n,o=!1)=>{!r||!u(e)||e.reduce((t,i,d,p)=>{if(t){if(d!==p.length-1)return!t[i]&&o&&(t[i]={}),t[i];t[i]=n}},r)},_=(r,e)=>{const n=new Set;if(!Array.isArray(e))return n;for(const[o,t,i]of e)if(!(!Array.isArray(t)||!t.length))switch(n.add(t[0]),o){case"delete":a(r,t);break;case"update":case"set":l(r,t,i,!0);break;default:break}return n};
@@ -0,0 +1 @@
1
+ var p=Object.defineProperty;var l=Object.getOwnPropertyDescriptor;var g=Object.getOwnPropertyNames;var d=Object.prototype.hasOwnProperty;var y=(t,o)=>{for(var e in o)p(t,e,{get:o[e],enumerable:!0})},u=(t,o,e,n)=>{if(o&&typeof o=="object"||typeof o=="function")for(let s of g(o))!d.call(t,s)&&s!==e&&p(t,s,{get:()=>o[s],enumerable:!(n=l(o,s))||n.enumerable});return t};var A=t=>u(p({},"__esModule",{value:!0}),t);var O={};y(O,{onOps:()=>w,onSnapshot:()=>S});module.exports=A(O);var f=require("smbls"),c=require("@symbo.ls/utils"),h=require("./applier.js");const S=(t,o,e)=>(n={})=>{let{data:s}=n;const{schema:i}=n;s&&(s=t.call("deepDestringifyFunctions",s,Array.isArray(s)?[]:{}),Object.entries(s).forEach(([r,a])=>{e[r]&&typeof e[r]=="object"?r==="designSystem"?(0,f.init)(a):(0,c.overwriteShallow)(e[r],a):e[r]=a}),i&&(e.schema=i))},w=(t,o,e)=>(n={})=>{let{changes:s}=n;if(!s||!Array.isArray(s)||!s.length)return;s=t.call("deepDestringifyFunctions",s,Array.isArray(s)?[]:{});const i=(0,h.applyOpsToCtx)(e,s);if(i.has("state")&&o.update(e.state),["pages","components","snippets","functions","assets"].some(r=>i.has(r))){const{pathname:r,search:a,hash:m}=e.window.location;t.call("router",r+a+m,t.__ref.root,{},{scrollToTop:!1})}i.has("designSystem")&&(0,f.init)(e.designSystem)};
@@ -0,0 +1 @@
1
+ var d=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var g=Object.getOwnPropertyNames;var U=Object.prototype.hasOwnProperty;var T=(e,n)=>{for(var o in n)d(e,o,{get:n[o],enumerable:!0})},E=(e,n,o,t)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of g(n))!U.call(e,r)&&r!==o&&d(e,r,{get:()=>n[r],enumerable:!(t=w(n,r))||t.enumerable});return e};var A=e=>E(d({},"__esModule",{value:!0}),e);var R={};T(R,{DefaultSyncApp:()=>O,FORBIDDEN_SEGMENTS:()=>s.FORBIDDEN_SEGMENTS,Notifications:()=>c.Notifications,SyncComponent:()=>u,applyOpsToCtx:()=>s.applyOpsToCtx,connectToSocket:()=>v,deletePath:()=>s.deletePath,isSafePath:()=>s.isSafePath,notificationsPlugin:()=>M,onOps:()=>i.onOps,onSnapshot:()=>i.onSnapshot,setPath:()=>s.setPath,syncPlugin:()=>B});module.exports=A(R);var G=require("@symbo.ls/router"),f=require("socket.io-client"),l=require("@symbo.ls/utils"),s=require("./applier.js"),i=require("./handlers.js"),c=require("./SyncNotifications.js");const C=!1,D=()=>C?"http://localhost:8080":"https://api.symbols.app",I=e=>{const n=e&&e.editor&&e.editor.socketUrl||e&&e.settings&&e.settings.socketUrl;return typeof n=="string"&&n?{mode:"collab",baseUrl:n}:e&&e.editor&&e.editor.runtime==="runner"?{mode:"runner",baseUrl:null}:{mode:null,baseUrl:null}},j=async e=>{try{const n=e||D(),o=await l.window.fetch(`${n}/service-token`,{method:"GET"});let t;try{const r=await o.clone().json();if(r&&typeof r.token=="string")return r.token.trim();t=await o.text()}catch{t=await o.text()}return(t||"").replace(/\s+/gu,"")||void 0}catch(n){console.error("[sync] Failed to fetch service-token",n)}},y=(e,n,o,t)=>{e.on("snapshot",(0,i.onSnapshot)(n,o,t)),e.on("ops",(0,i.onOps)(n,o,t)),e.on("clients",r=>{t.editor?.verbose&&(0,c.connectedToSymbols)(r,n,o)}),e.on("disconnect",r=>{t.editor?.verbose&&console.info("[sync] disconnected",r)})},N=async(e,n,o,t)=>{const r=await j(t);if(!r)return console.warn("[sync] no service token \u2014 collab disabled"),null;const p=o.key;if(!p)return console.warn("[sync] ctx.key missing \u2014 collab cannot authenticate"),null;const S=o.editor?.projectId||null,a=(0,f.io)(t,{path:"/collab-socket",transports:["websocket"],auth:{token:r,projectId:S,projectKey:p,branch:"main",live:!0,clientType:"platform"},reconnectionAttempts:1/0,reconnectionDelayMax:4e3});return a.on("connect",()=>{o.editor?.verbose&&console.info("[sync] connected (collab)")}),y(a,e,n,o),a},P=(e,n,o)=>{const t=(0,f.io)({transports:["websocket","polling"],reconnectionAttempts:1/0,reconnectionDelayMax:4e3});return t.on("connect",()=>{o.editor?.verbose!==!1&&console.info("[sync] connected (runner)")}),y(t,e,n,o),t.on("reload",()=>{typeof l.window<"u"&&l.window.location&&l.window.location.reload()}),t},v=async(e,n,o)=>{const{mode:t,baseUrl:r}=I(o);return t?t==="collab"?N(e,n,o,r):t==="runner"?P(e,n,o):null:null},u={onInit:v},O={extends:[u,c.Notifications]},m=e=>typeof e>"u",b=e=>!!(e?.editor?.socketUrl||e?.settings?.socketUrl),h=e=>e?.editor?.runtime==="runner",k=(e,n)=>{Array.isArray(e.extends)?e.extends.push(n):e.extends?e.extends=[e.extends,n]:e.extends=[n]},B={name:"sync",beforeCreate(e,n){const o=n&&n.editor;(o&&!m(o.liveSync)?o.liveSync:b(n)||h(n))&&(b(n)&&!h(n)&&!n?.key||k(e,u))}},M={name:"notifications",beforeCreate(e,n){if(!n)return;const o=m(n.notifications)?null:n.notifications,t=n.editor&&n.editor.verbose===!0;(o===!0||o!==!1&&t)&&k(e,c.Notifications)}};
@@ -0,0 +1 @@
1
+ const i={success:"green",error:"red",warning:"yellow"},c=(o,t,n)=>{if(o.symbols){if(!n.connected){n.notifications.connected={title:"Connected",message:"to the Symbols live server"},n.update({connected:!0});const e=setTimeout(()=>{delete n.notifications.connected,t.notifications.content.connected.update({animation:"fadeOutDown"}),n.update({connected:!0}),clearTimeout(e)},3e3)}}else if(n.connected){n.notifications.connected={title:"Disconnected",type:"error"},n.update({connected:!1});const e=setTimeout(()=>{delete n.notifications.connected,t.notifications.content.connected&&t.notifications.content.connected.update({animation:"fadeOutDown"}),n.update({connected:!0}),clearTimeout(e)},3e3)}},d={state:{notifications:[]},Notifications:{position:"fixed",left:"A2",bottom:"Z2",zIndex:"999",childExtends:"Notification",childProps:({state:o})=>({animationDuration:"C",background:i[o.type||"success"],icon:null,Flex:{Title:{text:"{{ title }}"},P:{text:"{{ title }}"}},onRender:(t,n,e)=>{n.update({animation:"fadeInUp"})},onClick:(t,n,e)=>{delete e.notifications[n.key],n.update({animation:"fadeOutDown"}),e.onClose&&e.onClose(t,n,e)}}),IconText:null,childrenAs:"state",children:({state:o})=>o.notifications}};export{d as Notifications,c as connectedToSymbols};
@@ -0,0 +1 @@
1
+ const a=new Set(["__proto__","constructor","prototype"]),s=r=>{if(!Array.isArray(r))return!1;for(let t=0;t<r.length;t++)if(a.has(r[t]))return!1;return!0},l=(r,t)=>{!r||!s(t)||t.reduce((n,i,e,o)=>{if(n&&i in n){if(e!==o.length-1)return n[i];delete n[i]}},r)},d=(r,t,n,i=!1)=>{!r||!s(t)||t.reduce((e,o,u,f)=>{if(e){if(u!==f.length-1)return!e[o]&&i&&(e[o]={}),e[o];e[o]=n}},r)},p=(r,t)=>{const n=new Set;if(!Array.isArray(t))return n;for(const[i,e,o]of t)if(!(!Array.isArray(e)||!e.length))switch(n.add(e[0]),i){case"delete":l(r,e);break;case"update":case"set":d(r,e,o,!0);break;default:break}return n};export{a as FORBIDDEN_SEGMENTS,p as applyOpsToCtx,l as deletePath,s as isSafePath,d as setPath};
@@ -0,0 +1 @@
1
+ import{init as p}from"smbls";import{overwriteShallow as c}from"@symbo.ls/utils";import{applyOpsToCtx as h}from"./applier.js";const d=(r,a,e)=>(i={})=>{let{data:s}=i;const{schema:o}=i;s&&(s=r.call("deepDestringifyFunctions",s,Array.isArray(s)?[]:{}),Object.entries(s).forEach(([t,n])=>{e[t]&&typeof e[t]=="object"?t==="designSystem"?p(n):c(e[t],n):e[t]=n}),o&&(e.schema=o))},y=(r,a,e)=>(i={})=>{let{changes:s}=i;if(!s||!Array.isArray(s)||!s.length)return;s=r.call("deepDestringifyFunctions",s,Array.isArray(s)?[]:{});const o=h(e,s);if(o.has("state")&&a.update(e.state),["pages","components","snippets","functions","assets"].some(t=>o.has(t))){const{pathname:t,search:n,hash:f}=e.window.location;r.call("router",t+n+f,r.__ref.root,{},{scrollToTop:!1})}o.has("designSystem")&&p(e.designSystem)};export{y as onOps,d as onSnapshot};
@@ -0,0 +1 @@
1
+ import"@symbo.ls/router";import{io as a}from"socket.io-client";import{window as s}from"@symbo.ls/utils";import{FORBIDDEN_SEGMENTS as v,isSafePath as S,deletePath as w,setPath as g,applyOpsToCtx as U}from"./applier.js";import{onSnapshot as d,onOps as f}from"./handlers.js";import{connectedToSymbols as T,Notifications as c}from"./SyncNotifications.js";const E=!1,A=()=>E?"http://localhost:8080":"https://api.symbols.app",C=e=>{const n=e&&e.editor&&e.editor.socketUrl||e&&e.settings&&e.settings.socketUrl;return typeof n=="string"&&n?{mode:"collab",baseUrl:n}:e&&e.editor&&e.editor.runtime==="runner"?{mode:"runner",baseUrl:null}:{mode:null,baseUrl:null}},D=async e=>{try{const n=e||A(),o=await s.fetch(`${n}/service-token`,{method:"GET"});let t;try{const r=await o.clone().json();if(r&&typeof r.token=="string")return r.token.trim();t=await o.text()}catch{t=await o.text()}return(t||"").replace(/\s+/gu,"")||void 0}catch(n){console.error("[sync] Failed to fetch service-token",n)}},u=(e,n,o,t)=>{e.on("snapshot",d(n,o,t)),e.on("ops",f(n,o,t)),e.on("clients",r=>{t.editor?.verbose&&T(r,n,o)}),e.on("disconnect",r=>{t.editor?.verbose&&console.info("[sync] disconnected",r)})},I=async(e,n,o,t)=>{const r=await D(t);if(!r)return console.warn("[sync] no service token \u2014 collab disabled"),null;const l=o.key;if(!l)return console.warn("[sync] ctx.key missing \u2014 collab cannot authenticate"),null;const k=o.editor?.projectId||null,i=a(t,{path:"/collab-socket",transports:["websocket"],auth:{token:r,projectId:k,projectKey:l,branch:"main",live:!0,clientType:"platform"},reconnectionAttempts:1/0,reconnectionDelayMax:4e3});return i.on("connect",()=>{o.editor?.verbose&&console.info("[sync] connected (collab)")}),u(i,e,n,o),i},j=(e,n,o)=>{const t=a({transports:["websocket","polling"],reconnectionAttempts:1/0,reconnectionDelayMax:4e3});return t.on("connect",()=>{o.editor?.verbose!==!1&&console.info("[sync] connected (runner)")}),u(t,e,n,o),t.on("reload",()=>{typeof s<"u"&&s.location&&s.location.reload()}),t},N=async(e,n,o)=>{const{mode:t,baseUrl:r}=C(o);return t?t==="collab"?I(e,n,o,r):t==="runner"?j(e,n,o):null:null},p={onInit:N},V={extends:[p,c]},y=e=>typeof e>"u",m=e=>!!(e?.editor?.socketUrl||e?.settings?.socketUrl),b=e=>e?.editor?.runtime==="runner",h=(e,n)=>{Array.isArray(e.extends)?e.extends.push(n):e.extends?e.extends=[e.extends,n]:e.extends=[n]},_={name:"sync",beforeCreate(e,n){const o=n&&n.editor;(o&&!y(o.liveSync)?o.liveSync:m(n)||b(n))&&(m(n)&&!b(n)&&!n?.key||h(e,p))}},K={name:"notifications",beforeCreate(e,n){if(!n)return;const o=y(n.notifications)?null:n.notifications,t=n.editor&&n.editor.verbose===!0;(o===!0||o!==!1&&t)&&h(e,c)}};export{V as DefaultSyncApp,v as FORBIDDEN_SEGMENTS,c as Notifications,p as SyncComponent,U as applyOpsToCtx,N as connectToSocket,w as deletePath,S as isSafePath,K as notificationsPlugin,f as onOps,d as onSnapshot,g as setPath,_ as syncPlugin};
package/package.json CHANGED
@@ -1,41 +1,53 @@
1
1
  {
2
2
  "name": "@symbo.ls/sync",
3
- "version": "3.14.1",
4
- "main": "./index.js",
5
- "module": "./index.js",
3
+ "version": "3.14.3",
4
+ "main": "./dist/cjs/index.cjs",
5
+ "module": "./dist/esm/index.js",
6
6
  "gitHead": "9fc1b79b41cdc725ca6b24aec64920a599634681",
7
7
  "files": [
8
8
  "dist",
9
- "*.js"
9
+ "*.md",
10
+ "LICENSE"
10
11
  ],
11
12
  "repository": "https://github.com/symbo-ls/scratch",
12
13
  "type": "module",
13
14
  "license": "CC-BY-NC-4.0",
14
- "unpkg": "./dist/iife/index.js",
15
- "jsdelivr": "./dist/iife/index.js",
16
15
  "exports": {
17
- ".": "./index.js",
18
- "./applier": "./applier.js",
19
- "./handlers": "./handlers.js",
20
- "./package.json": "./package.json"
16
+ ".": {
17
+ "development": "./index.js",
18
+ "import": "./dist/esm/index.js",
19
+ "require": "./dist/cjs/index.cjs",
20
+ "default": "./dist/esm/index.js"
21
+ },
22
+ "./package.json": "./package.json",
23
+ "./applier": {
24
+ "development": "./applier.js",
25
+ "import": "./dist/esm/applier.js",
26
+ "require": "./dist/cjs/applier.cjs",
27
+ "default": "./dist/esm/applier.js"
28
+ },
29
+ "./handlers": {
30
+ "development": "./handlers.js",
31
+ "import": "./dist/esm/handlers.js",
32
+ "require": "./dist/cjs/handlers.cjs",
33
+ "default": "./dist/esm/handlers.js"
34
+ }
21
35
  },
22
36
  "source": "index.js",
23
37
  "publishConfig": {
24
- "access": "public"
38
+ "access": "restricted"
25
39
  },
26
40
  "scripts": {
27
- "copy:package:cjs": "cp ../../build/package-cjs.json dist/cjs/package.json",
28
- "build:esm": "NODE_ENV=$NODE_ENV esbuild *.js --target=es2020 --format=esm --outdir=dist/esm --define:process.env.NODE_ENV=process.env.NODE_ENV",
29
- "build:cjs": "NODE_ENV=$NODE_ENV esbuild *.js --target=node18 --format=cjs --outdir=dist/cjs --define:process.env.NODE_ENV=process.env.NODE_ENV",
30
- "build:iife": "NODE_ENV=$NODE_ENV esbuild index.js --bundle --target=es2020 --format=iife --global-name=SmblsSync --outfile=dist/iife/index.js --define:process.env.NODE_ENV=process.env.NODE_ENV --external:smbls --external:@symbo.ls/* --external:socket.io-client --external:css-in-props",
31
- "build": "node ../../build/build.js",
32
- "prepublish": "npm run build && npm run copy:package:cjs"
41
+ "build:esm": "esbuild $(find . -type f -name \"*.js\" -not -path \"*/dist/*\" -not -path \"*/node_modules/*\" -not -path \"*/__tests__/*\" -not -path \"*/test/*\" -not -path \"*/demo/*\" -not -path \"*/demos/*\" -not -path \"*/examples/*\" -not -path \"*/fixtures/*\" -not -name \"*.test.js\") --target=es2022 --format=esm --outbase=. --outdir=dist/esm --minify --log-level=warning",
42
+ "build:cjs": "esbuild $(find . -type f -name \"*.js\" -not -path \"*/dist/*\" -not -path \"*/node_modules/*\" -not -path \"*/__tests__/*\" -not -path \"*/test/*\" -not -path \"*/demo/*\" -not -path \"*/demos/*\" -not -path \"*/examples/*\" -not -path \"*/fixtures/*\" -not -name \"*.test.js\") --target=node20 --format=cjs --outbase=. --outdir=dist/cjs --out-extension:.js=.cjs --minify --log-level=warning",
43
+ "build": "rimraf dist && npm run build:esm && npm run build:cjs",
44
+ "prepublishOnly": "npm run build"
33
45
  },
34
46
  "dependencies": {
35
47
  "@symbo.ls/router": "^3.14.1",
36
48
  "@symbo.ls/utils": "^3.14.2",
37
49
  "@symbo.ls/scratch": "^3.14.2",
38
- "@symbo.ls/default-config": "^3.14.2",
50
+ "@symbo.ls/default-config": "^3.14.4",
39
51
  "chalk": "^5.4.1",
40
52
  "socket.io-client": "^4.8.1"
41
53
  },
@@ -1,120 +0,0 @@
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 DELETED
@@ -1,71 +0,0 @@
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 DELETED
@@ -1,83 +0,0 @@
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/src/init.js'
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 DELETED
@@ -1,258 +0,0 @@
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
- }