@tldraw/editor 5.5.0 → 5.6.0-canary.669b4c97f643
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/index.js +1 -1
- package/dist-cjs/lib/editor/Editor.js +15 -4
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/shared/resizeBox.js +21 -16
- package/dist-cjs/lib/editor/shapes/shared/resizeBox.js.map +2 -2
- package/dist-cjs/lib/hooks/useCursor.js +12 -8
- package/dist-cjs/lib/hooks/useCursor.js.map +2 -2
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +12 -5
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/editor/Editor.mjs +17 -5
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/shared/resizeBox.mjs +21 -16
- package/dist-esm/lib/editor/shapes/shared/resizeBox.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCursor.mjs +12 -8
- package/dist-esm/lib/hooks/useCursor.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +13 -6
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +17 -19
- package/src/lib/editor/Editor.ts +38 -9
- package/src/lib/editor/shapes/shared/resizeBox.ts +21 -16
- package/src/lib/hooks/useCursor.ts +14 -7
- package/src/lib/test/useCursor.test.tsx +36 -0
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +73 -0
- package/src/lib/utils/sync/TLLocalSyncClient.ts +24 -8
- package/src/version.ts +3 -3
|
@@ -21,6 +21,7 @@ __export(resizeBox_exports, {
|
|
|
21
21
|
resizeBox: () => resizeBox
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(resizeBox_exports);
|
|
24
|
+
var import_utils = require("../../../primitives/utils");
|
|
24
25
|
var import_Vec = require("../../../primitives/Vec");
|
|
25
26
|
function resizeBox(shape, info, opts = {}) {
|
|
26
27
|
const { newPoint, handle, scaleX, scaleY } = info;
|
|
@@ -29,29 +30,31 @@ function resizeBox(shape, info, opts = {}) {
|
|
|
29
30
|
let h = shape.props.h * scaleY;
|
|
30
31
|
const offset = new import_Vec.Vec(0, 0);
|
|
31
32
|
if (w > 0) {
|
|
32
|
-
|
|
33
|
+
const clampedW = (0, import_utils.clamp)(w, minWidth, maxWidth);
|
|
34
|
+
if (clampedW !== w) {
|
|
33
35
|
switch (handle) {
|
|
34
36
|
case "top_left":
|
|
35
37
|
case "left":
|
|
36
38
|
case "bottom_left": {
|
|
37
|
-
offset.x = w -
|
|
39
|
+
offset.x = w - clampedW;
|
|
38
40
|
break;
|
|
39
41
|
}
|
|
40
42
|
case "top":
|
|
41
43
|
case "bottom": {
|
|
42
|
-
offset.x = (w -
|
|
44
|
+
offset.x = (w - clampedW) / 2;
|
|
43
45
|
break;
|
|
44
46
|
}
|
|
45
47
|
default: {
|
|
46
48
|
offset.x = 0;
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
|
-
w =
|
|
51
|
+
w = clampedW;
|
|
50
52
|
}
|
|
51
53
|
} else {
|
|
52
54
|
offset.x = w;
|
|
53
55
|
w = -w;
|
|
54
|
-
|
|
56
|
+
const clampedW = (0, import_utils.clamp)(w, minWidth, maxWidth);
|
|
57
|
+
if (clampedW !== w) {
|
|
55
58
|
switch (handle) {
|
|
56
59
|
case "top_left":
|
|
57
60
|
case "left":
|
|
@@ -60,36 +63,38 @@ function resizeBox(shape, info, opts = {}) {
|
|
|
60
63
|
break;
|
|
61
64
|
}
|
|
62
65
|
default: {
|
|
63
|
-
offset.x = -
|
|
66
|
+
offset.x = -clampedW;
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
|
-
w =
|
|
69
|
+
w = clampedW;
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
if (h > 0) {
|
|
70
|
-
|
|
73
|
+
const clampedH = (0, import_utils.clamp)(h, minHeight, maxHeight);
|
|
74
|
+
if (clampedH !== h) {
|
|
71
75
|
switch (handle) {
|
|
72
76
|
case "top_left":
|
|
73
77
|
case "top":
|
|
74
78
|
case "top_right": {
|
|
75
|
-
offset.y = h -
|
|
79
|
+
offset.y = h - clampedH;
|
|
76
80
|
break;
|
|
77
81
|
}
|
|
78
82
|
case "right":
|
|
79
83
|
case "left": {
|
|
80
|
-
offset.y = (h -
|
|
84
|
+
offset.y = (h - clampedH) / 2;
|
|
81
85
|
break;
|
|
82
86
|
}
|
|
83
87
|
default: {
|
|
84
88
|
offset.y = 0;
|
|
85
89
|
}
|
|
86
90
|
}
|
|
87
|
-
h =
|
|
91
|
+
h = clampedH;
|
|
88
92
|
}
|
|
89
93
|
} else {
|
|
90
94
|
offset.y = h;
|
|
91
95
|
h = -h;
|
|
92
|
-
|
|
96
|
+
const clampedH = (0, import_utils.clamp)(h, minHeight, maxHeight);
|
|
97
|
+
if (clampedH !== h) {
|
|
93
98
|
switch (handle) {
|
|
94
99
|
case "top_left":
|
|
95
100
|
case "top":
|
|
@@ -98,10 +103,10 @@ function resizeBox(shape, info, opts = {}) {
|
|
|
98
103
|
break;
|
|
99
104
|
}
|
|
100
105
|
default: {
|
|
101
|
-
offset.y = -
|
|
106
|
+
offset.y = -clampedH;
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
|
-
h =
|
|
109
|
+
h = clampedH;
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
const { x, y } = offset.rot(shape.rotation).add(newPoint);
|
|
@@ -110,8 +115,8 @@ function resizeBox(shape, info, opts = {}) {
|
|
|
110
115
|
x,
|
|
111
116
|
y,
|
|
112
117
|
props: {
|
|
113
|
-
w
|
|
114
|
-
h
|
|
118
|
+
w,
|
|
119
|
+
h
|
|
115
120
|
}
|
|
116
121
|
};
|
|
117
122
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/lib/editor/shapes/shared/resizeBox.ts"],
|
|
4
|
-
"sourcesContent": ["import { VecModel } from '@tldraw/tlschema'\nimport { Box } from '../../../primitives/Box'\nimport { Vec } from '../../../primitives/Vec'\nimport { TLResizeHandle } from '../../types/selection-types'\nimport type { TLBaseBoxShape } from '../BaseBoxShapeUtil'\nimport { TLResizeMode } from '../ShapeUtil'\n\n/** @public */\nexport interface ResizeBoxOptions {\n\tminWidth?: number\n\tmaxWidth?: number\n\tminHeight?: number\n\tmaxHeight?: number\n}\n\n/** @public */\nexport function resizeBox<T extends TLBaseBoxShape>(\n\tshape: T,\n\tinfo: {\n\t\tnewPoint: VecModel\n\t\thandle: TLResizeHandle\n\t\tmode: TLResizeMode\n\t\tscaleX: number\n\t\tscaleY: number\n\t\tinitialBounds: Box\n\t\tinitialShape: T\n\t},\n\topts = {} as ResizeBoxOptions\n): T {\n\tconst { newPoint, handle, scaleX, scaleY } = info\n\tconst { minWidth = 1, maxWidth = Infinity, minHeight = 1, maxHeight = Infinity } = opts\n\n\tlet w = shape.props.w * scaleX\n\tlet h = shape.props.h * scaleY\n\n\tconst offset = new Vec(0, 0)\n\n\tif (w > 0) {\n\t\
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,iBAAoB;AAcb,SAAS,UACf,OACA,MASA,OAAO,CAAC,GACJ;AACJ,QAAM,EAAE,UAAU,QAAQ,QAAQ,OAAO,IAAI;AAC7C,QAAM,EAAE,WAAW,GAAG,WAAW,UAAU,YAAY,GAAG,YAAY,SAAS,IAAI;AAEnF,MAAI,IAAI,MAAM,MAAM,IAAI;AACxB,MAAI,IAAI,MAAM,MAAM,IAAI;AAExB,QAAM,SAAS,IAAI,eAAI,GAAG,CAAC;AAE3B,MAAI,IAAI,GAAG;AACV,
|
|
4
|
+
"sourcesContent": ["import { VecModel } from '@tldraw/tlschema'\nimport { Box } from '../../../primitives/Box'\nimport { clamp } from '../../../primitives/utils'\nimport { Vec } from '../../../primitives/Vec'\nimport { TLResizeHandle } from '../../types/selection-types'\nimport type { TLBaseBoxShape } from '../BaseBoxShapeUtil'\nimport { TLResizeMode } from '../ShapeUtil'\n\n/** @public */\nexport interface ResizeBoxOptions {\n\tminWidth?: number\n\tmaxWidth?: number\n\tminHeight?: number\n\tmaxHeight?: number\n}\n\n/** @public */\nexport function resizeBox<T extends TLBaseBoxShape>(\n\tshape: T,\n\tinfo: {\n\t\tnewPoint: VecModel\n\t\thandle: TLResizeHandle\n\t\tmode: TLResizeMode\n\t\tscaleX: number\n\t\tscaleY: number\n\t\tinitialBounds: Box\n\t\tinitialShape: T\n\t},\n\topts = {} as ResizeBoxOptions\n): T {\n\tconst { newPoint, handle, scaleX, scaleY } = info\n\tconst { minWidth = 1, maxWidth = Infinity, minHeight = 1, maxHeight = Infinity } = opts\n\n\tlet w = shape.props.w * scaleX\n\tlet h = shape.props.h * scaleY\n\n\tconst offset = new Vec(0, 0)\n\n\tif (w > 0) {\n\t\tconst clampedW = clamp(w, minWidth, maxWidth)\n\t\tif (clampedW !== w) {\n\t\t\tswitch (handle) {\n\t\t\t\tcase 'top_left':\n\t\t\t\tcase 'left':\n\t\t\t\tcase 'bottom_left': {\n\t\t\t\t\toffset.x = w - clampedW\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'top':\n\t\t\t\tcase 'bottom': {\n\t\t\t\t\toffset.x = (w - clampedW) / 2\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tdefault: {\n\t\t\t\t\toffset.x = 0\n\t\t\t\t}\n\t\t\t}\n\t\t\tw = clampedW\n\t\t}\n\t} else {\n\t\toffset.x = w\n\t\tw = -w\n\t\tconst clampedW = clamp(w, minWidth, maxWidth)\n\t\tif (clampedW !== w) {\n\t\t\tswitch (handle) {\n\t\t\t\tcase 'top_left':\n\t\t\t\tcase 'left':\n\t\t\t\tcase 'bottom_left': {\n\t\t\t\t\toffset.x = -w\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tdefault: {\n\t\t\t\t\toffset.x = -clampedW\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tw = clampedW\n\t\t}\n\t}\n\n\tif (h > 0) {\n\t\tconst clampedH = clamp(h, minHeight, maxHeight)\n\t\tif (clampedH !== h) {\n\t\t\tswitch (handle) {\n\t\t\t\tcase 'top_left':\n\t\t\t\tcase 'top':\n\t\t\t\tcase 'top_right': {\n\t\t\t\t\toffset.y = h - clampedH\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase 'right':\n\t\t\t\tcase 'left': {\n\t\t\t\t\toffset.y = (h - clampedH) / 2\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tdefault: {\n\t\t\t\t\toffset.y = 0\n\t\t\t\t}\n\t\t\t}\n\n\t\t\th = clampedH\n\t\t}\n\t} else {\n\t\toffset.y = h\n\t\th = -h\n\t\tconst clampedH = clamp(h, minHeight, maxHeight)\n\t\tif (clampedH !== h) {\n\t\t\tswitch (handle) {\n\t\t\t\tcase 'top_left':\n\t\t\t\tcase 'top':\n\t\t\t\tcase 'top_right': {\n\t\t\t\t\toffset.y = -h\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tdefault: {\n\t\t\t\t\toffset.y = -clampedH\n\t\t\t\t}\n\t\t\t}\n\t\t\th = clampedH\n\t\t}\n\t}\n\n\tconst { x, y } = offset.rot(shape.rotation).add(newPoint)\n\n\treturn {\n\t\t...shape,\n\t\tx,\n\t\ty,\n\t\tprops: {\n\t\t\tw,\n\t\t\th,\n\t\t},\n\t}\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,mBAAsB;AACtB,iBAAoB;AAcb,SAAS,UACf,OACA,MASA,OAAO,CAAC,GACJ;AACJ,QAAM,EAAE,UAAU,QAAQ,QAAQ,OAAO,IAAI;AAC7C,QAAM,EAAE,WAAW,GAAG,WAAW,UAAU,YAAY,GAAG,YAAY,SAAS,IAAI;AAEnF,MAAI,IAAI,MAAM,MAAM,IAAI;AACxB,MAAI,IAAI,MAAM,MAAM,IAAI;AAExB,QAAM,SAAS,IAAI,eAAI,GAAG,CAAC;AAE3B,MAAI,IAAI,GAAG;AACV,UAAM,eAAW,oBAAM,GAAG,UAAU,QAAQ;AAC5C,QAAI,aAAa,GAAG;AACnB,cAAQ,QAAQ;AAAA,QACf,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK,eAAe;AACnB,iBAAO,IAAI,IAAI;AACf;AAAA,QACD;AAAA,QACA,KAAK;AAAA,QACL,KAAK,UAAU;AACd,iBAAO,KAAK,IAAI,YAAY;AAC5B;AAAA,QACD;AAAA,QACA,SAAS;AACR,iBAAO,IAAI;AAAA,QACZ;AAAA,MACD;AACA,UAAI;AAAA,IACL;AAAA,EACD,OAAO;AACN,WAAO,IAAI;AACX,QAAI,CAAC;AACL,UAAM,eAAW,oBAAM,GAAG,UAAU,QAAQ;AAC5C,QAAI,aAAa,GAAG;AACnB,cAAQ,QAAQ;AAAA,QACf,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK,eAAe;AACnB,iBAAO,IAAI,CAAC;AACZ;AAAA,QACD;AAAA,QACA,SAAS;AACR,iBAAO,IAAI,CAAC;AAAA,QACb;AAAA,MACD;AAEA,UAAI;AAAA,IACL;AAAA,EACD;AAEA,MAAI,IAAI,GAAG;AACV,UAAM,eAAW,oBAAM,GAAG,WAAW,SAAS;AAC9C,QAAI,aAAa,GAAG;AACnB,cAAQ,QAAQ;AAAA,QACf,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK,aAAa;AACjB,iBAAO,IAAI,IAAI;AACf;AAAA,QACD;AAAA,QACA,KAAK;AAAA,QACL,KAAK,QAAQ;AACZ,iBAAO,KAAK,IAAI,YAAY;AAC5B;AAAA,QACD;AAAA,QACA,SAAS;AACR,iBAAO,IAAI;AAAA,QACZ;AAAA,MACD;AAEA,UAAI;AAAA,IACL;AAAA,EACD,OAAO;AACN,WAAO,IAAI;AACX,QAAI,CAAC;AACL,UAAM,eAAW,oBAAM,GAAG,WAAW,SAAS;AAC9C,QAAI,aAAa,GAAG;AACnB,cAAQ,QAAQ;AAAA,QACf,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK,aAAa;AACjB,iBAAO,IAAI,CAAC;AACZ;AAAA,QACD;AAAA,QACA,SAAS;AACR,iBAAO,IAAI,CAAC;AAAA,QACb;AAAA,MACD;AACA,UAAI;AAAA,IACL;AAAA,EACD;AAEA,QAAM,EAAE,GAAG,EAAE,IAAI,OAAO,IAAI,MAAM,QAAQ,EAAE,IAAI,QAAQ;AAExD,SAAO;AAAA,IACN,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA,OAAO;AAAA,MACN;AAAA,MACA;AAAA,IACD;AAAA,EACD;AACD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -35,9 +35,9 @@ function getCursorCss(svg, r, tr, f, color, hotspotX = 16, hotspotY = 16) {
|
|
|
35
35
|
const c = Math.cos(a);
|
|
36
36
|
const dx = 1 * c - 1 * s;
|
|
37
37
|
const dy = 1 * s + 1 * c;
|
|
38
|
-
return `url("data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: ${color};'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='${dx}' dy='${dy}' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(${r + tr} 16 16)${f ? ` scale(-1,-1) translate(0, -32)` : ""}' filter='url(%23shadow)'>` + svg.replaceAll(`"`, `'`) + `</g></svg>") ${hotspotX} ${hotspotY}, pointer`;
|
|
38
|
+
return `url("data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: ${encodeURIComponent(color)};'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='${dx}' dy='${dy}' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(${r + tr} 16 16)${f ? ` scale(-1,-1) translate(0, -32)` : ""}' filter='url(%23shadow)'>` + svg.replaceAll(`"`, `'`) + `</g></svg>") ${hotspotX} ${hotspotY}, pointer`;
|
|
39
39
|
}
|
|
40
|
-
const STATIC_CURSORS = [
|
|
40
|
+
const STATIC_CURSORS = /* @__PURE__ */ new Set([
|
|
41
41
|
"default",
|
|
42
42
|
"pointer",
|
|
43
43
|
"cross",
|
|
@@ -47,9 +47,12 @@ const STATIC_CURSORS = [
|
|
|
47
47
|
"grabbing",
|
|
48
48
|
"text",
|
|
49
49
|
"zoom-in",
|
|
50
|
-
"zoom-out"
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
"zoom-out",
|
|
51
|
+
"rotate",
|
|
52
|
+
"resize-edge",
|
|
53
|
+
"resize-corner"
|
|
54
|
+
]);
|
|
55
|
+
const DYNAMIC_CURSORS = {
|
|
53
56
|
none: () => "none",
|
|
54
57
|
"ew-resize": (r, f, c) => getCursorCss(EDGE_SVG, r, 0, f, c),
|
|
55
58
|
"ns-resize": (r, f, c) => getCursorCss(EDGE_SVG, r, 90, f, c),
|
|
@@ -61,7 +64,8 @@ const CURSORS = {
|
|
|
61
64
|
"swne-rotate": (r, f, c) => getCursorCss(ROTATE_CORNER_SVG, r, 270, f, c)
|
|
62
65
|
};
|
|
63
66
|
function getCursor(cursor, rotation = 0, color = "black") {
|
|
64
|
-
|
|
67
|
+
if (STATIC_CURSORS.has(cursor)) return `var(--tl-cursor-${cursor})`;
|
|
68
|
+
return DYNAMIC_CURSORS[cursor]((0, import_utils.radiansToDegrees)(rotation), false, color);
|
|
65
69
|
}
|
|
66
70
|
function useCursor() {
|
|
67
71
|
const editor = (0, import_useEditor.useEditor)();
|
|
@@ -70,8 +74,8 @@ function useCursor() {
|
|
|
70
74
|
"useCursor",
|
|
71
75
|
() => {
|
|
72
76
|
const { type, rotation } = editor.getInstanceState().cursor;
|
|
73
|
-
if (STATIC_CURSORS.
|
|
74
|
-
container.style.setProperty("--tl-cursor",
|
|
77
|
+
if (STATIC_CURSORS.has(type)) {
|
|
78
|
+
container.style.setProperty("--tl-cursor", getCursor(type));
|
|
75
79
|
return;
|
|
76
80
|
}
|
|
77
81
|
const { cursor } = editor.getCurrentTheme().colors[editor.getColorMode()];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/hooks/useCursor.ts"],
|
|
4
|
-
"sourcesContent": ["import { useQuickReactor } from '@tldraw/state-react'\nimport { TLCursorType } from '@tldraw/tlschema'\nimport { PI, radiansToDegrees } from '../primitives/utils'\nimport { useContainer } from './useContainer'\nimport { useEditor } from './useEditor'\n\nconst CORNER_SVG = `<path d='m19.7432 17.0869-4.072 4.068 2.829 2.828-8.473-.013-.013-8.47 2.841 2.842 4.075-4.068 1.414-1.415-2.844-2.842h8.486v8.484l-2.83-2.827z' fill='%23fff'/><path d='m18.6826 16.7334-4.427 4.424 1.828 1.828-5.056-.016-.014-5.054 1.842 1.841 4.428-4.422 2.474-2.475-1.844-1.843h5.073v5.071l-1.83-1.828z' fill='%23000'/>`\nconst EDGE_SVG = `<path d='m9 17.9907v.005l5.997 5.996.001-3.999h1.999 2.02v4l5.98-6.001-5.98-5.999.001 4.019-2.021.002h-2l.001-4.022zm1.411.003 3.587-3.588-.001 2.587h3.5 2.521v-2.585l3.565 3.586-3.564 3.585-.001-2.585h-2.521l-3.499-.001-.001 2.586z' fill='%23fff'/><path d='m17.4971 18.9932h2.521v2.586l3.565-3.586-3.565-3.585v2.605h-2.521-3.5v-2.607l-3.586 3.587 3.586 3.586v-2.587z' fill='%23000'/>`\nconst ROTATE_CORNER_SVG = `<path d=\"M22.4789 9.45728L25.9935 12.9942L22.4789 16.5283V14.1032C18.126 14.1502 14.6071 17.6737 14.5675 22.0283H17.05L13.513 25.543L9.97889 22.0283H12.5674C12.6071 16.5691 17.0214 12.1503 22.4789 12.1031L22.4789 9.45728Z\" fill=\"black\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M21.4789 7.03223L27.4035 12.9945L21.4789 18.9521V15.1868C18.4798 15.6549 16.1113 18.0273 15.649 21.0284H19.475L13.5128 26.953L7.55519 21.0284H11.6189C12.1243 15.8155 16.2679 11.6677 21.4789 11.1559L21.4789 7.03223ZM22.4789 12.1031C17.0214 12.1503 12.6071 16.5691 12.5674 22.0284H9.97889L13.513 25.543L17.05 22.0284H14.5675C14.5705 21.6896 14.5947 21.3558 14.6386 21.0284C15.1157 17.4741 17.9266 14.6592 21.4789 14.1761C21.8063 14.1316 22.1401 14.1069 22.4789 14.1032V16.5284L25.9935 12.9942L22.4789 9.45729L22.4789 12.1031Z\" fill=\"white\"/>`\n\nfunction getCursorCss(\n\tsvg: string,\n\tr: number,\n\ttr: number,\n\tf: boolean,\n\tcolor: string,\n\thotspotX = 16,\n\thotspotY = 16\n) {\n\tconst a = (-tr - r) * (PI / 180)\n\tconst s = Math.sin(a)\n\tconst c = Math.cos(a)\n\tconst dx = 1 * c - 1 * s\n\tconst dy = 1 * s + 1 * c\n\n\treturn (\n\t\t`url(\"data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: ${color};'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='${dx}' dy='${dy}' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(${\n\t\t\tr + tr\n\t\t} 16 16)${f ? ` scale(-1,-1) translate(0, -32)` : ''}' filter='url(%23shadow)'>` +\n\t\tsvg.replaceAll(`\"`, `'`) +\n\t\t`</g></svg>\") ${hotspotX} ${hotspotY}, pointer`\n\t)\n}\n\nconst STATIC_CURSORS = [\n\t'default',\n\t'pointer',\n\t'cross',\n\t'comment',\n\t'move',\n\t'grab',\n\t'grabbing',\n\t'text',\n\t'zoom-in',\n\t'zoom-out',\n]\n\ntype CursorFunction = (rotation: number, flip: boolean, color: string) => string\n\nconst
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAgC;AAEhC,mBAAqC;AACrC,0BAA6B;AAC7B,uBAA0B;AAE1B,MAAM,aAAa;AACnB,MAAM,WAAW;AACjB,MAAM,oBAAoB;AAE1B,SAAS,aACR,KACA,GACA,IACA,GACA,OACA,WAAW,IACX,WAAW,IACV;AACD,QAAM,KAAK,CAAC,KAAK,MAAM,kBAAK;AAC5B,QAAM,IAAI,KAAK,IAAI,CAAC;AACpB,QAAM,IAAI,KAAK,IAAI,CAAC;AACpB,QAAM,KAAK,IAAI,IAAI,IAAI;AACvB,QAAM,KAAK,IAAI,IAAI,IAAI;
|
|
4
|
+
"sourcesContent": ["import { useQuickReactor } from '@tldraw/state-react'\nimport { TLCursorType } from '@tldraw/tlschema'\nimport { PI, radiansToDegrees } from '../primitives/utils'\nimport { useContainer } from './useContainer'\nimport { useEditor } from './useEditor'\n\nconst CORNER_SVG = `<path d='m19.7432 17.0869-4.072 4.068 2.829 2.828-8.473-.013-.013-8.47 2.841 2.842 4.075-4.068 1.414-1.415-2.844-2.842h8.486v8.484l-2.83-2.827z' fill='%23fff'/><path d='m18.6826 16.7334-4.427 4.424 1.828 1.828-5.056-.016-.014-5.054 1.842 1.841 4.428-4.422 2.474-2.475-1.844-1.843h5.073v5.071l-1.83-1.828z' fill='%23000'/>`\nconst EDGE_SVG = `<path d='m9 17.9907v.005l5.997 5.996.001-3.999h1.999 2.02v4l5.98-6.001-5.98-5.999.001 4.019-2.021.002h-2l.001-4.022zm1.411.003 3.587-3.588-.001 2.587h3.5 2.521v-2.585l3.565 3.586-3.564 3.585-.001-2.585h-2.521l-3.499-.001-.001 2.586z' fill='%23fff'/><path d='m17.4971 18.9932h2.521v2.586l3.565-3.586-3.565-3.585v2.605h-2.521-3.5v-2.607l-3.586 3.587 3.586 3.586v-2.587z' fill='%23000'/>`\nconst ROTATE_CORNER_SVG = `<path d=\"M22.4789 9.45728L25.9935 12.9942L22.4789 16.5283V14.1032C18.126 14.1502 14.6071 17.6737 14.5675 22.0283H17.05L13.513 25.543L9.97889 22.0283H12.5674C12.6071 16.5691 17.0214 12.1503 22.4789 12.1031L22.4789 9.45728Z\" fill=\"black\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M21.4789 7.03223L27.4035 12.9945L21.4789 18.9521V15.1868C18.4798 15.6549 16.1113 18.0273 15.649 21.0284H19.475L13.5128 26.953L7.55519 21.0284H11.6189C12.1243 15.8155 16.2679 11.6677 21.4789 11.1559L21.4789 7.03223ZM22.4789 12.1031C17.0214 12.1503 12.6071 16.5691 12.5674 22.0284H9.97889L13.513 25.543L17.05 22.0284H14.5675C14.5705 21.6896 14.5947 21.3558 14.6386 21.0284C15.1157 17.4741 17.9266 14.6592 21.4789 14.1761C21.8063 14.1316 22.1401 14.1069 22.4789 14.1032V16.5284L25.9935 12.9942L22.4789 9.45729L22.4789 12.1031Z\" fill=\"white\"/>`\n\nfunction getCursorCss(\n\tsvg: string,\n\tr: number,\n\ttr: number,\n\tf: boolean,\n\tcolor: string,\n\thotspotX = 16,\n\thotspotY = 16\n) {\n\tconst a = (-tr - r) * (PI / 180)\n\tconst s = Math.sin(a)\n\tconst c = Math.cos(a)\n\tconst dx = 1 * c - 1 * s\n\tconst dy = 1 * s + 1 * c\n\n\t// A raw '#' in the colour (hex) would end the unencoded data url as a fragment.\n\treturn (\n\t\t`url(\"data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: ${encodeURIComponent(color)};'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='${dx}' dy='${dy}' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(${\n\t\t\tr + tr\n\t\t} 16 16)${f ? ` scale(-1,-1) translate(0, -32)` : ''}' filter='url(%23shadow)'>` +\n\t\tsvg.replaceAll(`\"`, `'`) +\n\t\t`</g></svg>\") ${hotspotX} ${hotspotY}, pointer`\n\t)\n}\n\n// Cursors that map straight to a prerendered `--tl-cursor-*` css variable rather than a runtime svg.\nconst STATIC_CURSORS = new Set<TLCursorType>([\n\t'default',\n\t'pointer',\n\t'cross',\n\t'comment',\n\t'move',\n\t'grab',\n\t'grabbing',\n\t'text',\n\t'zoom-in',\n\t'zoom-out',\n\t'rotate',\n\t'resize-edge',\n\t'resize-corner',\n])\n\ntype CursorFunction = (rotation: number, flip: boolean, color: string) => string\n\nconst DYNAMIC_CURSORS: Record<TLCursorType, CursorFunction> = {\n\tnone: () => 'none',\n\t'ew-resize': (r, f, c) => getCursorCss(EDGE_SVG, r, 0, f, c),\n\t'ns-resize': (r, f, c) => getCursorCss(EDGE_SVG, r, 90, f, c),\n\t'nesw-resize': (r, f, c) => getCursorCss(CORNER_SVG, r, 0, f, c),\n\t'nwse-resize': (r, f, c) => getCursorCss(CORNER_SVG, r, 90, f, c),\n\t'nwse-rotate': (r, f, c) => getCursorCss(ROTATE_CORNER_SVG, r, 0, f, c),\n\t'nesw-rotate': (r, f, c) => getCursorCss(ROTATE_CORNER_SVG, r, 90, f, c),\n\t'senw-rotate': (r, f, c) => getCursorCss(ROTATE_CORNER_SVG, r, 180, f, c),\n\t'swne-rotate': (r, f, c) => getCursorCss(ROTATE_CORNER_SVG, r, 270, f, c),\n}\n\n/** @public */\nexport function getCursor(cursor: TLCursorType, rotation = 0, color = 'black') {\n\tif (STATIC_CURSORS.has(cursor)) return `var(--tl-cursor-${cursor})`\n\treturn DYNAMIC_CURSORS[cursor](radiansToDegrees(rotation), false, color)\n}\n\nexport function useCursor() {\n\tconst editor = useEditor()\n\tconst container = useContainer()\n\n\tuseQuickReactor(\n\t\t'useCursor',\n\t\t() => {\n\t\t\tconst { type, rotation } = editor.getInstanceState().cursor\n\n\t\t\t// Static cursors don't need the theme colour, so skip reading it to avoid depending on it.\n\t\t\tif (STATIC_CURSORS.has(type)) {\n\t\t\t\tcontainer.style.setProperty('--tl-cursor', getCursor(type))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst { cursor } = editor.getCurrentTheme().colors[editor.getColorMode()]\n\t\t\tcontainer.style.setProperty('--tl-cursor', getCursor(type, rotation, cursor))\n\t\t},\n\t\t[editor, container]\n\t)\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAgC;AAEhC,mBAAqC;AACrC,0BAA6B;AAC7B,uBAA0B;AAE1B,MAAM,aAAa;AACnB,MAAM,WAAW;AACjB,MAAM,oBAAoB;AAE1B,SAAS,aACR,KACA,GACA,IACA,GACA,OACA,WAAW,IACX,WAAW,IACV;AACD,QAAM,KAAK,CAAC,KAAK,MAAM,kBAAK;AAC5B,QAAM,IAAI,KAAK,IAAI,CAAC;AACpB,QAAM,IAAI,KAAK,IAAI,CAAC;AACpB,QAAM,KAAK,IAAI,IAAI,IAAI;AACvB,QAAM,KAAK,IAAI,IAAI,IAAI;AAGvB,SACC,4HAA4H,mBAAmB,KAAK,CAAC,mIAAmI,EAAE,SAAS,EAAE,6FACpS,IAAI,EACL,UAAU,IAAI,oCAAoC,EAAE,+BACpD,IAAI,WAAW,KAAK,GAAG,IACvB,gBAAgB,QAAQ,IAAI,QAAQ;AAEtC;AAGA,MAAM,iBAAiB,oBAAI,IAAkB;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAID,MAAM,kBAAwD;AAAA,EAC7D,MAAM,MAAM;AAAA,EACZ,aAAa,CAAC,GAAG,GAAG,MAAM,aAAa,UAAU,GAAG,GAAG,GAAG,CAAC;AAAA,EAC3D,aAAa,CAAC,GAAG,GAAG,MAAM,aAAa,UAAU,GAAG,IAAI,GAAG,CAAC;AAAA,EAC5D,eAAe,CAAC,GAAG,GAAG,MAAM,aAAa,YAAY,GAAG,GAAG,GAAG,CAAC;AAAA,EAC/D,eAAe,CAAC,GAAG,GAAG,MAAM,aAAa,YAAY,GAAG,IAAI,GAAG,CAAC;AAAA,EAChE,eAAe,CAAC,GAAG,GAAG,MAAM,aAAa,mBAAmB,GAAG,GAAG,GAAG,CAAC;AAAA,EACtE,eAAe,CAAC,GAAG,GAAG,MAAM,aAAa,mBAAmB,GAAG,IAAI,GAAG,CAAC;AAAA,EACvE,eAAe,CAAC,GAAG,GAAG,MAAM,aAAa,mBAAmB,GAAG,KAAK,GAAG,CAAC;AAAA,EACxE,eAAe,CAAC,GAAG,GAAG,MAAM,aAAa,mBAAmB,GAAG,KAAK,GAAG,CAAC;AACzE;AAGO,SAAS,UAAU,QAAsB,WAAW,GAAG,QAAQ,SAAS;AAC9E,MAAI,eAAe,IAAI,MAAM,EAAG,QAAO,mBAAmB,MAAM;AAChE,SAAO,gBAAgB,MAAM,MAAE,+BAAiB,QAAQ,GAAG,OAAO,KAAK;AACxE;AAEO,SAAS,YAAY;AAC3B,QAAM,aAAS,4BAAU;AACzB,QAAM,gBAAY,kCAAa;AAE/B;AAAA,IACC;AAAA,IACA,MAAM;AACL,YAAM,EAAE,MAAM,SAAS,IAAI,OAAO,iBAAiB,EAAE;AAGrD,UAAI,eAAe,IAAI,IAAI,GAAG;AAC7B,kBAAU,MAAM,YAAY,eAAe,UAAU,IAAI,CAAC;AAC1D;AAAA,MACD;AAEA,YAAM,EAAE,OAAO,IAAI,OAAO,gBAAgB,EAAE,OAAO,OAAO,aAAa,CAAC;AACxE,gBAAU,MAAM,YAAY,eAAe,UAAU,MAAM,UAAU,MAAM,CAAC;AAAA,IAC7E;AAAA,IACA,CAAC,QAAQ,SAAS;AAAA,EACnB;AACD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -43,6 +43,7 @@ class BroadcastChannelMock {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
const BC = typeof BroadcastChannel === "undefined" ? BroadcastChannelMock : BroadcastChannel;
|
|
46
|
+
const pendingFlushes = /* @__PURE__ */ new Map();
|
|
46
47
|
class TLLocalSyncClient {
|
|
47
48
|
constructor(store, {
|
|
48
49
|
persistenceKey,
|
|
@@ -177,6 +178,8 @@ class TLLocalSyncClient {
|
|
|
177
178
|
this.disposables.add(() => {
|
|
178
179
|
this.channel.close();
|
|
179
180
|
});
|
|
181
|
+
await pendingFlushes.get(this.persistenceKey);
|
|
182
|
+
if (this.didDispose) return;
|
|
180
183
|
try {
|
|
181
184
|
data = await this.db.load({ sessionId: this.sessionId });
|
|
182
185
|
} catch (error) {
|
|
@@ -237,7 +240,6 @@ class TLLocalSyncClient {
|
|
|
237
240
|
if (this.didDispose) return;
|
|
238
241
|
this.debug("closing");
|
|
239
242
|
this.didDispose = true;
|
|
240
|
-
this.store._flushHistory();
|
|
241
243
|
this.disposables.forEach((d) => d());
|
|
242
244
|
if (this.scheduledPersistTimeout) {
|
|
243
245
|
clearTimeout(this.scheduledPersistTimeout);
|
|
@@ -246,7 +248,13 @@ class TLLocalSyncClient {
|
|
|
246
248
|
if (typeof window !== "undefined" && window.tlsync === this) {
|
|
247
249
|
delete window.tlsync;
|
|
248
250
|
}
|
|
249
|
-
this.
|
|
251
|
+
const previous = pendingFlushes.get(this.persistenceKey);
|
|
252
|
+
const flush = (previous ? previous.then(() => this.flushAndCloseDb()) : this.flushAndCloseDb()).catch(import_utils.noop).then(() => {
|
|
253
|
+
if (pendingFlushes.get(this.persistenceKey) === flush) {
|
|
254
|
+
pendingFlushes.delete(this.persistenceKey);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
pendingFlushes.set(this.persistenceKey, flush);
|
|
250
258
|
}
|
|
251
259
|
/**
|
|
252
260
|
* Flush queued changes so edits made in the last PERSIST_THROTTLE_MS before unmount aren't lost,
|
|
@@ -256,11 +264,10 @@ class TLLocalSyncClient {
|
|
|
256
264
|
*/
|
|
257
265
|
flushAndCloseDb() {
|
|
258
266
|
if (this.persistPromise) {
|
|
259
|
-
this.persistPromise.then(() => this.flushAndCloseDb());
|
|
260
|
-
return;
|
|
267
|
+
return this.persistPromise.then(() => this.flushAndCloseDb());
|
|
261
268
|
}
|
|
262
269
|
if (this.didLoad && this.diffQueue.length > 0) this.persistIfNeeded();
|
|
263
|
-
this.db.close();
|
|
270
|
+
return this.db.close();
|
|
264
271
|
}
|
|
265
272
|
isPersisting = false;
|
|
266
273
|
persistPromise = null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/lib/utils/sync/TLLocalSyncClient.ts"],
|
|
4
|
-
"sourcesContent": ["import { Signal, transact } from '@tldraw/state'\nimport { RecordsDiff, SerializedSchema, UnknownRecord, squashRecordDiffs } from '@tldraw/store'\nimport { TLStore } from '@tldraw/tlschema'\nimport { assert } from '@tldraw/utils'\nimport {\n\tTAB_ID,\n\tTLSessionStateSnapshot,\n\tcreateSessionStateSnapshotSignal,\n\textractSessionStateFromLegacySnapshot,\n\tloadSessionStateSnapshotIntoStore,\n} from '../../config/TLSessionStateSnapshot'\nimport { refreshPage } from '../runtime'\nimport { showCantReadFromIndexDbAlert, showCantWriteToIndexDbAlert } from './alerts'\nimport { LocalIndexedDb } from './LocalIndexedDb'\n\n/** How should we debounce persists? */\nconst PERSIST_THROTTLE_MS = 350\n/** If we're in an error state, how long should we wait before retrying a write? */\nconst PERSIST_RETRY_THROTTLE_MS = 10_000\n\nconst UPDATE_INSTANCE_STATE = Symbol('UPDATE_INSTANCE_STATE')\n\n/**\n * IMPORTANT!!!\n *\n * This is just a quick-n-dirty temporary solution that will be replaced with the remote sync client\n * once it has the db integrated\n */\n\ninterface SyncMessage {\n\ttype: 'diff'\n\tstoreId: string\n\tchanges: RecordsDiff<UnknownRecord>\n\tschema: SerializedSchema\n}\n\n// Sent by new clients when they connect\n// If another client is on the channel with a newer schema version\n// It will\ninterface AnnounceMessage {\n\ttype: 'announce'\n\tschema: SerializedSchema\n}\n\ntype Message = SyncMessage | AnnounceMessage\n\ntype UnpackPromise<T> = T extends Promise<infer U> ? U : T\n\nconst msg = (msg: Message) => msg\n\n/** @internal */\nexport class BroadcastChannelMock {\n\tonmessage?: (e: MessageEvent) => void\n\tconstructor(_name: string) {\n\t\t// noop\n\t}\n\tpostMessage(_msg: Message) {\n\t\t// noop\n\t}\n\tclose() {\n\t\t// noop\n\t}\n}\n\nconst BC = typeof BroadcastChannel === 'undefined' ? BroadcastChannelMock : BroadcastChannel\n\n/** @internal */\nexport class TLLocalSyncClient {\n\tprivate disposables = new Set<() => void>()\n\tprivate diffQueue: Array<RecordsDiff<UnknownRecord> | typeof UPDATE_INSTANCE_STATE> = []\n\tprivate didDispose = false\n\tprivate didLoad = false\n\tprivate shouldDoFullDBWrite = true\n\tprivate isReloading = false\n\treadonly persistenceKey: string\n\treadonly sessionId: string\n\treadonly serializedSchema: SerializedSchema\n\tprivate isDebugging = false\n\tprivate readonly documentTypes: ReadonlySet<string>\n\tprivate readonly $sessionStateSnapshot: Signal<TLSessionStateSnapshot | null>\n\t/** @internal */\n\treadonly db: LocalIndexedDb\n\n\tinitTime = Date.now()\n\tprivate debug(...args: any[]) {\n\t\tif (this.isDebugging) {\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.debug(...args)\n\t\t}\n\t}\n\tconstructor(\n\t\tpublic readonly store: TLStore,\n\t\t{\n\t\t\tpersistenceKey,\n\t\t\tsessionId = TAB_ID,\n\t\t\tonLoad,\n\t\t\tonLoadError,\n\t\t}: {\n\t\t\tpersistenceKey: string\n\t\t\tsessionId?: string\n\t\t\tonLoad(self: TLLocalSyncClient): void\n\t\t\tonLoadError(error: Error): void\n\t\t},\n\t\tpublic readonly channel = new BC(`tldraw-tab-sync-${persistenceKey}`)\n\t) {\n\t\tif (typeof window !== 'undefined') {\n\t\t\t;(window as any).tlsync = this\n\t\t}\n\t\tthis.persistenceKey = persistenceKey\n\t\tthis.sessionId = sessionId\n\t\tthis.db = new LocalIndexedDb(persistenceKey)\n\n\t\tthis.serializedSchema = this.store.schema.serialize()\n\t\tthis.$sessionStateSnapshot = createSessionStateSnapshotSignal(this.store)\n\n\t\tthis.disposables.add(\n\t\t\t// Set up a subscription to changes from the store: When\n\t\t\t// the store changes (and if the change was made by the user)\n\t\t\t// then immediately send the diff to other tabs via postMessage\n\t\t\t// and schedule a persist.\n\t\t\tstore.listen(\n\t\t\t\t({ changes }) => {\n\t\t\t\t\tthis.diffQueue.push(changes)\n\t\t\t\t\tthis.channel.postMessage(\n\t\t\t\t\t\tmsg({\n\t\t\t\t\t\t\ttype: 'diff',\n\t\t\t\t\t\t\tstoreId: this.store.id,\n\t\t\t\t\t\t\tchanges,\n\t\t\t\t\t\t\tschema: this.serializedSchema,\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t\tthis.schedulePersist()\n\t\t\t\t},\n\t\t\t\t{ source: 'user', scope: 'document' }\n\t\t\t)\n\t\t)\n\t\tthis.disposables.add(\n\t\t\tstore.listen(\n\t\t\t\t() => {\n\t\t\t\t\tthis.diffQueue.push(UPDATE_INSTANCE_STATE)\n\t\t\t\t\tthis.schedulePersist()\n\t\t\t\t},\n\t\t\t\t{ scope: 'session' }\n\t\t\t)\n\t\t)\n\n\t\tif (typeof window !== 'undefined') {\n\t\t\t// Persists are throttled, so without this the last PERSIST_THROTTLE_MS of edits are lost\n\t\t\t// whenever the tab is closed or reloaded. `pagehide` is the last reliable event before\n\t\t\t// unload on desktop; on mobile the page can be discarded without it firing, so we also\n\t\t\t// flush when the tab is hidden.\n\t\t\tconst flush = () => {\n\t\t\t\t// Before the initial load completes a full write would overwrite the saved\n\t\t\t\t// document with an empty store.\n\t\t\t\tif (!this.didLoad) return\n\t\t\t\tthis.persistIfNeeded()\n\t\t\t}\n\t\t\tconst onVisibilityChange = () => {\n\t\t\t\tif (document.visibilityState === 'hidden') flush()\n\t\t\t}\n\t\t\twindow.addEventListener('pagehide', flush)\n\t\t\tdocument.addEventListener('visibilitychange', onVisibilityChange)\n\t\t\tthis.disposables.add(() => {\n\t\t\t\twindow.removeEventListener('pagehide', flush)\n\t\t\t\tdocument.removeEventListener('visibilitychange', onVisibilityChange)\n\t\t\t})\n\t\t}\n\n\t\tthis.connect(onLoad, onLoadError)\n\n\t\tthis.documentTypes = new Set(\n\t\t\tObject.values(this.store.schema.types)\n\t\t\t\t.filter((t) => t.scope === 'document')\n\t\t\t\t.map((t) => t.typeName)\n\t\t)\n\t}\n\n\tprivate async connect(onLoad: (client: this) => void, onLoadError: (error: Error) => void) {\n\t\tthis.debug('connecting')\n\t\tlet data: UnpackPromise<ReturnType<LocalIndexedDb['load']>> | undefined\n\n\t\tconst handleMessage = (msg: Message) => {\n\t\t\t// if their schema is earlier than ours, we need to tell them so they can refresh\n\t\t\t// if their schema is later than ours, we need to refresh\n\t\t\tconst res = this.store.schema.getMigrationsSince(msg.schema)\n\n\t\t\tif (!res.ok) {\n\t\t\t\tthis.isReloading = true\n\t\t\t\t// we are older, refresh\n\t\t\t\t// but add a safety check to make sure we don't get in an infinite loop\n\t\t\t\tconst timeSinceInit = Date.now() - this.initTime\n\t\t\t\tif (timeSinceInit < 5000) {\n\t\t\t\t\t// This tab was just reloaded, but is out of date compared to other tabs.\n\t\t\t\t\t// Not expecting this to ever happen. It should only happen if we roll back a release that incremented\n\t\t\t\t\t// the schema version (which we should never do)\n\t\t\t\t\t// Or maybe during development if you have multiple local tabs open running the app on prod mode and you\n\t\t\t\t\t// check out an older commit. Dev server should be fine.\n\t\t\t\t\tonLoadError(new Error('Schema mismatch, please close other tabs and reload the page'))\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tthis.debug('reloading')\n\t\t\t\tif (typeof window !== 'undefined') refreshPage()\n\t\t\t\treturn false\n\t\t\t} else if (res.value.length > 0) {\n\t\t\t\t// they are older, tell them to refresh and not write any more data\n\t\t\t\tthis.debug('telling them to reload')\n\t\t\t\tthis.channel.postMessage({ type: 'announce', schema: this.serializedSchema })\n\t\t\t\t// schedule a full db write in case they wrote data anyway\n\t\t\t\tthis.shouldDoFullDBWrite = true\n\t\t\t\tthis.schedulePersist()\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// otherwise, all good, same version :)\n\t\t\tif (msg.type === 'diff') {\n\t\t\t\tthis.debug('applying diff')\n\t\t\t\ttransact(() => {\n\t\t\t\t\tthis.store.mergeRemoteChanges(() => {\n\t\t\t\t\t\tthis.store.applyDiff(msg.changes as any)\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn true\n\t\t}\n\n\t\t// Listen from the start and buffer until we've loaded: diffs other tabs broadcast while\n\t\t// we're still reading from IndexedDB would otherwise be dropped, then erased by our first\n\t\t// (full) db write.\n\t\tlet bufferedMessages: Message[] | null = []\n\t\tthis.channel.onmessage = ({ data }) => {\n\t\t\tthis.debug('got message', data)\n\t\t\tif (bufferedMessages) {\n\t\t\t\tbufferedMessages.push(data as Message)\n\t\t\t} else {\n\t\t\t\thandleMessage(data as Message)\n\t\t\t}\n\t\t}\n\t\tthis.disposables.add(() => {\n\t\t\tthis.channel.close()\n\t\t})\n\n\t\ttry {\n\t\t\tdata = await this.db.load({ sessionId: this.sessionId })\n\t\t} catch (error: any) {\n\t\t\tonLoadError(error)\n\t\t\tshowCantReadFromIndexDbAlert()\n\t\t\treturn\n\t\t}\n\n\t\tthis.debug('loaded data from store', data, 'didDispose', this.didDispose)\n\t\tif (this.didDispose) return\n\n\t\ttry {\n\t\t\tif (data) {\n\t\t\t\tconst documentSnapshot = Object.fromEntries(data.records.map((r) => [r.id, r]))\n\t\t\t\tconst sessionStateSnapshot =\n\t\t\t\t\tdata.sessionStateSnapshot ?? extractSessionStateFromLegacySnapshot(documentSnapshot)\n\t\t\t\tconst migrationResult = this.store.schema.migrateStoreSnapshot({\n\t\t\t\t\tstore: documentSnapshot,\n\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\tschema: data.schema ?? this.store.schema.serializeEarliestVersion(),\n\t\t\t\t})\n\n\t\t\t\tif (migrationResult.type === 'error') {\n\t\t\t\t\tconsole.error('failed to migrate store', migrationResult)\n\t\t\t\t\tonLoadError(new Error(`Failed to migrate store: ${migrationResult.reason}`))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tconst records = Object.values(migrationResult.value).filter((r) =>\n\t\t\t\t\tthis.documentTypes.has(r.typeName)\n\t\t\t\t)\n\t\t\t\tif (records.length > 0) {\n\t\t\t\t\t// 3. Merge the changes into the REAL STORE\n\t\t\t\t\tthis.store.mergeRemoteChanges(() => {\n\t\t\t\t\t\t// Calling put will validate the records!\n\t\t\t\t\t\tthis.store.put(records, 'initialize')\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tif (sessionStateSnapshot) {\n\t\t\t\t\tloadSessionStateSnapshotIntoStore(this.store, sessionStateSnapshot, {\n\t\t\t\t\t\tforceOverwrite: true,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Older-schema announcements schedule writes, so drain every diff before those writes\n\t\t\t// or an onLoad callback that immediately closes the client can capture a snapshot.\n\t\t\tconst messages = bufferedMessages\n\t\t\tbufferedMessages = null\n\t\t\tfor (const message of messages) {\n\t\t\t\tif (!handleMessage(message)) return\n\t\t\t}\n\t\t\tthis.didLoad = true\n\t\t\tif (this.diffQueue.length > 0) this.schedulePersist()\n\n\t\t\tthis.channel.postMessage({ type: 'announce', schema: this.serializedSchema })\n\t\t\tonLoad(this)\n\n\t\t\t// Session state rows accumulate one per tab, so trim them on load. Pruning is best-effort\n\t\t\t// and must never turn a successful load into a failure.\n\t\t\tthis.db.pruneSessions({ keepSessionId: this.sessionId }).catch((e: unknown) => {\n\t\t\t\tthis.debug('failed to prune session state rows', e)\n\t\t\t})\n\t\t} catch (e: any) {\n\t\t\tthis.debug('error loading data from store', e)\n\t\t\tif (this.didDispose) return\n\t\t\tonLoadError(e)\n\t\t\treturn\n\t\t}\n\t}\n\n\tclose() {\n\t\tif (this.didDispose) return\n\t\tthis.debug('closing')\n\t\tthis.didDispose = true\n\t\t// Store listeners normally run on the next frame; collect the last edits before removal.\n\t\tthis.store._flushHistory()\n\t\tthis.disposables.forEach((d) => d())\n\t\tif (this.scheduledPersistTimeout) {\n\t\t\tclearTimeout(this.scheduledPersistTimeout)\n\t\t\tthis.scheduledPersistTimeout = null\n\t\t}\n\t\tif (typeof window !== 'undefined' && (window as any).tlsync === this) {\n\t\t\tdelete (window as any).tlsync\n\t\t}\n\t\tthis.flushAndCloseDb()\n\t}\n\n\t/**\n\t * Flush queued changes so edits made in the last PERSIST_THROTTLE_MS before unmount aren't lost,\n\t * then close the db. Never flushes before load (a full write then would wipe the db with an empty\n\t * store), and only when something is queued: shouldDoFullDBWrite stays true until the first\n\t * persist, so an unconditional flush would rewrite the whole document on every no-op close.\n\t */\n\tprivate flushAndCloseDb() {\n\t\t// A persist in flight can't be joined (persistIfNeeded bails while one is running) and,\n\t\t// now that we're disposed, won't schedule a follow-up. Edits queued during that write\n\t\t// would be lost if the db closed now, so wait for it and flush again.\n\t\tif (this.persistPromise) {\n\t\t\tthis.persistPromise.then(() => this.flushAndCloseDb())\n\t\t\treturn\n\t\t}\n\t\tif (this.didLoad && this.diffQueue.length > 0) this.persistIfNeeded()\n\t\t// the db waits for the transaction the flush just opened before actually closing\n\t\tthis.db.close()\n\t}\n\n\tprivate isPersisting = false\n\tprivate persistPromise: Promise<void> | null = null\n\tprivate didLastWriteError = false\n\t// eslint-disable-next-line no-restricted-globals\n\tprivate scheduledPersistTimeout: ReturnType<typeof setTimeout> | null = null\n\n\t/**\n\t * Schedule a persist. Persists don't happen immediately: they are throttled to avoid writing too\n\t * often, and will retry if failed.\n\t *\n\t * @internal\n\t */\n\tprivate schedulePersist() {\n\t\tthis.debug('schedulePersist', this.scheduledPersistTimeout)\n\t\tif (this.scheduledPersistTimeout) return\n\t\t// A deferred persist would try to write to a database that is closing or already closed.\n\t\tif (this.didDispose) return\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tthis.scheduledPersistTimeout = setTimeout(\n\t\t\t() => {\n\t\t\t\tthis.scheduledPersistTimeout = null\n\t\t\t\tthis.persistIfNeeded()\n\t\t\t},\n\t\t\tthis.didLastWriteError ? PERSIST_RETRY_THROTTLE_MS : PERSIST_THROTTLE_MS\n\t\t)\n\t}\n\n\t/**\n\t * Persist to IndexedDB only under certain circumstances:\n\t *\n\t * - If we're not already persisting\n\t * - If we're not reloading the page\n\t * - And we have something to persist (a full db write scheduled or changes in the diff queue)\n\t *\n\t * @internal\n\t */\n\tprivate persistIfNeeded() {\n\t\tthis.debug('persistIfNeeded', {\n\t\t\tisPersisting: this.isPersisting,\n\t\t\tisReloading: this.isReloading,\n\t\t\tshouldDoFullDBWrite: this.shouldDoFullDBWrite,\n\t\t\tdiffQueueLength: this.diffQueue.length,\n\t\t\tstoreIsPossiblyCorrupt: this.store.isPossiblyCorrupted(),\n\t\t})\n\n\t\t// if we've scheduled a persist for the future, that's no longer needed\n\t\tif (this.scheduledPersistTimeout) {\n\t\t\tclearTimeout(this.scheduledPersistTimeout)\n\t\t\tthis.scheduledPersistTimeout = null\n\t\t}\n\n\t\tif (!this.didLoad) return\n\n\t\t// if a persist is already in progress, we don't need to do anything -\n\t\t// if there are still outstanding changes once it's finished, it'll\n\t\t// schedule another persist\n\t\tif (this.isPersisting) return\n\n\t\t// if we're reloading the page, it's because there's a newer client\n\t\t// present so lets not overwrite their changes\n\t\tif (this.isReloading) return\n\n\t\t// if the store is possibly corrupted, we don't want to persist\n\t\tif (this.store.isPossiblyCorrupted()) return\n\n\t\t// if we're scheduled for a full write or if we have changes outstanding, let's persist them!\n\t\tif (this.shouldDoFullDBWrite || this.diffQueue.length > 0) {\n\t\t\tthis.persistPromise = this.doPersist()\n\t\t}\n\t}\n\n\t/**\n\t * Actually persist to IndexedDB. If the write fails, then we'll retry with a full db write after\n\t * a short delay.\n\t */\n\tprivate async doPersist() {\n\t\tassert(!this.isPersisting, 'persist already in progress')\n\t\tthis.isPersisting = true\n\n\t\tthis.debug('doPersist start')\n\n\t\t// instantly empty the diff queue, but keep our own copy of it. this way\n\t\t// diffs that come in during the persist will still get tracked\n\t\tconst diffQueue = this.diffQueue\n\t\tthis.diffQueue = []\n\n\t\ttry {\n\t\t\tif (this.shouldDoFullDBWrite) {\n\t\t\t\tthis.shouldDoFullDBWrite = false\n\t\t\t\tawait this.db.storeSnapshot({\n\t\t\t\t\tschema: this.store.schema,\n\t\t\t\t\tsnapshot: this.store.serialize(),\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t\tsessionStateSnapshot: this.$sessionStateSnapshot.get(),\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tconst diffs = squashRecordDiffs(\n\t\t\t\t\tdiffQueue.filter((d): d is RecordsDiff<UnknownRecord> => d !== UPDATE_INSTANCE_STATE)\n\t\t\t\t)\n\t\t\t\tawait this.db.storeChanges({\n\t\t\t\t\tchanges: diffs,\n\t\t\t\t\tschema: this.store.schema,\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t\tsessionStateSnapshot: this.$sessionStateSnapshot.get(),\n\t\t\t\t})\n\t\t\t}\n\t\t\tthis.didLastWriteError = false\n\t\t} catch (e) {\n\t\t\t// set this.shouldDoFullDBWrite because we clear the diffQueue no matter what,\n\t\t\t// so if this is just a temporary error, we will still persist all changes\n\t\t\tthis.shouldDoFullDBWrite = true\n\t\t\tthis.didLastWriteError = true\n\t\t\tconsole.error('failed to store changes in indexed db', e)\n\n\t\t\tif (!this.didDispose && typeof window !== 'undefined') {\n\t\t\t\tshowCantWriteToIndexDbAlert()\n\t\t\t\trefreshPage()\n\t\t\t}\n\t\t}\n\n\t\tthis.isPersisting = false\n\t\tthis.persistPromise = null\n\t\tthis.debug('doPersist end')\n\n\t\t// changes might have come in between when we started the persist and\n\t\t// now. we request another persist so any new changes can get written\n\t\tthis.schedulePersist()\n\t}\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAiC;AACjC,mBAAgF;AAEhF,
|
|
4
|
+
"sourcesContent": ["import { Signal, transact } from '@tldraw/state'\nimport { RecordsDiff, SerializedSchema, UnknownRecord, squashRecordDiffs } from '@tldraw/store'\nimport { TLStore } from '@tldraw/tlschema'\nimport { assert, noop } from '@tldraw/utils'\nimport {\n\tTAB_ID,\n\tTLSessionStateSnapshot,\n\tcreateSessionStateSnapshotSignal,\n\textractSessionStateFromLegacySnapshot,\n\tloadSessionStateSnapshotIntoStore,\n} from '../../config/TLSessionStateSnapshot'\nimport { refreshPage } from '../runtime'\nimport { showCantReadFromIndexDbAlert, showCantWriteToIndexDbAlert } from './alerts'\nimport { LocalIndexedDb } from './LocalIndexedDb'\n\n/** How should we debounce persists? */\nconst PERSIST_THROTTLE_MS = 350\n/** If we're in an error state, how long should we wait before retrying a write? */\nconst PERSIST_RETRY_THROTTLE_MS = 10_000\n\nconst UPDATE_INSTANCE_STATE = Symbol('UPDATE_INSTANCE_STATE')\n\n/**\n * IMPORTANT!!!\n *\n * This is just a quick-n-dirty temporary solution that will be replaced with the remote sync client\n * once it has the db integrated\n */\n\ninterface SyncMessage {\n\ttype: 'diff'\n\tstoreId: string\n\tchanges: RecordsDiff<UnknownRecord>\n\tschema: SerializedSchema\n}\n\n// Sent by new clients when they connect\n// If another client is on the channel with a newer schema version\n// It will\ninterface AnnounceMessage {\n\ttype: 'announce'\n\tschema: SerializedSchema\n}\n\ntype Message = SyncMessage | AnnounceMessage\n\ntype UnpackPromise<T> = T extends Promise<infer U> ? U : T\n\nconst msg = (msg: Message) => msg\n\n/** @internal */\nexport class BroadcastChannelMock {\n\tonmessage?: (e: MessageEvent) => void\n\tconstructor(_name: string) {\n\t\t// noop\n\t}\n\tpostMessage(_msg: Message) {\n\t\t// noop\n\t}\n\tclose() {\n\t\t// noop\n\t}\n}\n\nconst BC = typeof BroadcastChannel === 'undefined' ? BroadcastChannelMock : BroadcastChannel\n\n// Flushes still running for closed clients, by key. See connect() and close().\nconst pendingFlushes = new Map<string, Promise<void>>()\n\n/** @internal */\nexport class TLLocalSyncClient {\n\tprivate disposables = new Set<() => void>()\n\tprivate diffQueue: Array<RecordsDiff<UnknownRecord> | typeof UPDATE_INSTANCE_STATE> = []\n\tprivate didDispose = false\n\tprivate didLoad = false\n\tprivate shouldDoFullDBWrite = true\n\tprivate isReloading = false\n\treadonly persistenceKey: string\n\treadonly sessionId: string\n\treadonly serializedSchema: SerializedSchema\n\tprivate isDebugging = false\n\tprivate readonly documentTypes: ReadonlySet<string>\n\tprivate readonly $sessionStateSnapshot: Signal<TLSessionStateSnapshot | null>\n\t/** @internal */\n\treadonly db: LocalIndexedDb\n\n\tinitTime = Date.now()\n\tprivate debug(...args: any[]) {\n\t\tif (this.isDebugging) {\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.debug(...args)\n\t\t}\n\t}\n\tconstructor(\n\t\tpublic readonly store: TLStore,\n\t\t{\n\t\t\tpersistenceKey,\n\t\t\tsessionId = TAB_ID,\n\t\t\tonLoad,\n\t\t\tonLoadError,\n\t\t}: {\n\t\t\tpersistenceKey: string\n\t\t\tsessionId?: string\n\t\t\tonLoad(self: TLLocalSyncClient): void\n\t\t\tonLoadError(error: Error): void\n\t\t},\n\t\tpublic readonly channel = new BC(`tldraw-tab-sync-${persistenceKey}`)\n\t) {\n\t\tif (typeof window !== 'undefined') {\n\t\t\t;(window as any).tlsync = this\n\t\t}\n\t\tthis.persistenceKey = persistenceKey\n\t\tthis.sessionId = sessionId\n\t\tthis.db = new LocalIndexedDb(persistenceKey)\n\n\t\tthis.serializedSchema = this.store.schema.serialize()\n\t\tthis.$sessionStateSnapshot = createSessionStateSnapshotSignal(this.store)\n\n\t\tthis.disposables.add(\n\t\t\t// Set up a subscription to changes from the store: When\n\t\t\t// the store changes (and if the change was made by the user)\n\t\t\t// then immediately send the diff to other tabs via postMessage\n\t\t\t// and schedule a persist.\n\t\t\tstore.listen(\n\t\t\t\t({ changes }) => {\n\t\t\t\t\tthis.diffQueue.push(changes)\n\t\t\t\t\tthis.channel.postMessage(\n\t\t\t\t\t\tmsg({\n\t\t\t\t\t\t\ttype: 'diff',\n\t\t\t\t\t\t\tstoreId: this.store.id,\n\t\t\t\t\t\t\tchanges,\n\t\t\t\t\t\t\tschema: this.serializedSchema,\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t\tthis.schedulePersist()\n\t\t\t\t},\n\t\t\t\t{ source: 'user', scope: 'document' }\n\t\t\t)\n\t\t)\n\t\tthis.disposables.add(\n\t\t\tstore.listen(\n\t\t\t\t() => {\n\t\t\t\t\tthis.diffQueue.push(UPDATE_INSTANCE_STATE)\n\t\t\t\t\tthis.schedulePersist()\n\t\t\t\t},\n\t\t\t\t{ scope: 'session' }\n\t\t\t)\n\t\t)\n\n\t\tif (typeof window !== 'undefined') {\n\t\t\t// Persists are throttled, so without this the last PERSIST_THROTTLE_MS of edits are lost\n\t\t\t// whenever the tab is closed or reloaded. `pagehide` is the last reliable event before\n\t\t\t// unload on desktop; on mobile the page can be discarded without it firing, so we also\n\t\t\t// flush when the tab is hidden.\n\t\t\tconst flush = () => {\n\t\t\t\t// Before the initial load completes a full write would overwrite the saved\n\t\t\t\t// document with an empty store.\n\t\t\t\tif (!this.didLoad) return\n\t\t\t\tthis.persistIfNeeded()\n\t\t\t}\n\t\t\tconst onVisibilityChange = () => {\n\t\t\t\tif (document.visibilityState === 'hidden') flush()\n\t\t\t}\n\t\t\twindow.addEventListener('pagehide', flush)\n\t\t\tdocument.addEventListener('visibilitychange', onVisibilityChange)\n\t\t\tthis.disposables.add(() => {\n\t\t\t\twindow.removeEventListener('pagehide', flush)\n\t\t\t\tdocument.removeEventListener('visibilitychange', onVisibilityChange)\n\t\t\t})\n\t\t}\n\n\t\tthis.connect(onLoad, onLoadError)\n\n\t\tthis.documentTypes = new Set(\n\t\t\tObject.values(this.store.schema.types)\n\t\t\t\t.filter((t) => t.scope === 'document')\n\t\t\t\t.map((t) => t.typeName)\n\t\t)\n\t}\n\n\tprivate async connect(onLoad: (client: this) => void, onLoadError: (error: Error) => void) {\n\t\tthis.debug('connecting')\n\t\tlet data: UnpackPromise<ReturnType<LocalIndexedDb['load']>> | undefined\n\n\t\tconst handleMessage = (msg: Message) => {\n\t\t\t// if their schema is earlier than ours, we need to tell them so they can refresh\n\t\t\t// if their schema is later than ours, we need to refresh\n\t\t\tconst res = this.store.schema.getMigrationsSince(msg.schema)\n\n\t\t\tif (!res.ok) {\n\t\t\t\tthis.isReloading = true\n\t\t\t\t// we are older, refresh\n\t\t\t\t// but add a safety check to make sure we don't get in an infinite loop\n\t\t\t\tconst timeSinceInit = Date.now() - this.initTime\n\t\t\t\tif (timeSinceInit < 5000) {\n\t\t\t\t\t// This tab was just reloaded, but is out of date compared to other tabs.\n\t\t\t\t\t// Not expecting this to ever happen. It should only happen if we roll back a release that incremented\n\t\t\t\t\t// the schema version (which we should never do)\n\t\t\t\t\t// Or maybe during development if you have multiple local tabs open running the app on prod mode and you\n\t\t\t\t\t// check out an older commit. Dev server should be fine.\n\t\t\t\t\tonLoadError(new Error('Schema mismatch, please close other tabs and reload the page'))\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tthis.debug('reloading')\n\t\t\t\tif (typeof window !== 'undefined') refreshPage()\n\t\t\t\treturn false\n\t\t\t} else if (res.value.length > 0) {\n\t\t\t\t// they are older, tell them to refresh and not write any more data\n\t\t\t\tthis.debug('telling them to reload')\n\t\t\t\tthis.channel.postMessage({ type: 'announce', schema: this.serializedSchema })\n\t\t\t\t// schedule a full db write in case they wrote data anyway\n\t\t\t\tthis.shouldDoFullDBWrite = true\n\t\t\t\tthis.schedulePersist()\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// otherwise, all good, same version :)\n\t\t\tif (msg.type === 'diff') {\n\t\t\t\tthis.debug('applying diff')\n\t\t\t\ttransact(() => {\n\t\t\t\t\tthis.store.mergeRemoteChanges(() => {\n\t\t\t\t\t\tthis.store.applyDiff(msg.changes as any)\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn true\n\t\t}\n\n\t\t// Listen from the start and buffer until we've loaded: diffs other tabs broadcast while\n\t\t// we're still reading from IndexedDB would otherwise be dropped, then erased by our first\n\t\t// (full) db write.\n\t\tlet bufferedMessages: Message[] | null = []\n\t\tthis.channel.onmessage = ({ data }) => {\n\t\t\tthis.debug('got message', data)\n\t\t\tif (bufferedMessages) {\n\t\t\t\tbufferedMessages.push(data as Message)\n\t\t\t} else {\n\t\t\t\thandleMessage(data as Message)\n\t\t\t}\n\t\t}\n\t\tthis.disposables.add(() => {\n\t\t\tthis.channel.close()\n\t\t})\n\n\t\t// our first write is a full snapshot, so it would erase whatever a client that just closed\n\t\t// on this key is still flushing\n\t\tawait pendingFlushes.get(this.persistenceKey)\n\t\tif (this.didDispose) return\n\n\t\ttry {\n\t\t\tdata = await this.db.load({ sessionId: this.sessionId })\n\t\t} catch (error: any) {\n\t\t\tonLoadError(error)\n\t\t\tshowCantReadFromIndexDbAlert()\n\t\t\treturn\n\t\t}\n\n\t\tthis.debug('loaded data from store', data, 'didDispose', this.didDispose)\n\t\tif (this.didDispose) return\n\n\t\ttry {\n\t\t\tif (data) {\n\t\t\t\tconst documentSnapshot = Object.fromEntries(data.records.map((r) => [r.id, r]))\n\t\t\t\tconst sessionStateSnapshot =\n\t\t\t\t\tdata.sessionStateSnapshot ?? extractSessionStateFromLegacySnapshot(documentSnapshot)\n\t\t\t\tconst migrationResult = this.store.schema.migrateStoreSnapshot({\n\t\t\t\t\tstore: documentSnapshot,\n\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\tschema: data.schema ?? this.store.schema.serializeEarliestVersion(),\n\t\t\t\t})\n\n\t\t\t\tif (migrationResult.type === 'error') {\n\t\t\t\t\tconsole.error('failed to migrate store', migrationResult)\n\t\t\t\t\tonLoadError(new Error(`Failed to migrate store: ${migrationResult.reason}`))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tconst records = Object.values(migrationResult.value).filter((r) =>\n\t\t\t\t\tthis.documentTypes.has(r.typeName)\n\t\t\t\t)\n\t\t\t\tif (records.length > 0) {\n\t\t\t\t\t// 3. Merge the changes into the REAL STORE\n\t\t\t\t\tthis.store.mergeRemoteChanges(() => {\n\t\t\t\t\t\t// Calling put will validate the records!\n\t\t\t\t\t\tthis.store.put(records, 'initialize')\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tif (sessionStateSnapshot) {\n\t\t\t\t\tloadSessionStateSnapshotIntoStore(this.store, sessionStateSnapshot, {\n\t\t\t\t\t\tforceOverwrite: true,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Older-schema announcements schedule writes, so drain every diff before those writes\n\t\t\t// or an onLoad callback that immediately closes the client can capture a snapshot.\n\t\t\tconst messages = bufferedMessages\n\t\t\tbufferedMessages = null\n\t\t\tfor (const message of messages) {\n\t\t\t\tif (!handleMessage(message)) return\n\t\t\t}\n\t\t\tthis.didLoad = true\n\t\t\tif (this.diffQueue.length > 0) this.schedulePersist()\n\n\t\t\tthis.channel.postMessage({ type: 'announce', schema: this.serializedSchema })\n\t\t\tonLoad(this)\n\n\t\t\t// Session state rows accumulate one per tab, so trim them on load. Pruning is best-effort\n\t\t\t// and must never turn a successful load into a failure.\n\t\t\tthis.db.pruneSessions({ keepSessionId: this.sessionId }).catch((e: unknown) => {\n\t\t\t\tthis.debug('failed to prune session state rows', e)\n\t\t\t})\n\t\t} catch (e: any) {\n\t\t\tthis.debug('error loading data from store', e)\n\t\t\tif (this.didDispose) return\n\t\t\tonLoadError(e)\n\t\t\treturn\n\t\t}\n\t}\n\n\tclose() {\n\t\tif (this.didDispose) return\n\t\tthis.debug('closing')\n\t\tthis.didDispose = true\n\t\tthis.disposables.forEach((d) => d())\n\t\tif (this.scheduledPersistTimeout) {\n\t\t\tclearTimeout(this.scheduledPersistTimeout)\n\t\t\tthis.scheduledPersistTimeout = null\n\t\t}\n\t\tif (typeof window !== 'undefined' && (window as any).tlsync === this) {\n\t\t\tdelete (window as any).tlsync\n\t\t}\n\t\t// flush now when the key is idle: close() runs during teardown, where a deferred write may\n\t\t// never run. wait when it isn't, or our snapshot lands first and gets overwritten.\n\t\t// connect() awaits this, so it must never reject\n\t\tconst previous = pendingFlushes.get(this.persistenceKey)\n\t\tconst flush = (previous ? previous.then(() => this.flushAndCloseDb()) : this.flushAndCloseDb())\n\t\t\t.catch(noop)\n\t\t\t.then(() => {\n\t\t\t\tif (pendingFlushes.get(this.persistenceKey) === flush) {\n\t\t\t\t\tpendingFlushes.delete(this.persistenceKey)\n\t\t\t\t}\n\t\t\t})\n\t\tpendingFlushes.set(this.persistenceKey, flush)\n\t}\n\n\t/**\n\t * Flush queued changes so edits made in the last PERSIST_THROTTLE_MS before unmount aren't lost,\n\t * then close the db. Never flushes before load (a full write then would wipe the db with an empty\n\t * store), and only when something is queued: shouldDoFullDBWrite stays true until the first\n\t * persist, so an unconditional flush would rewrite the whole document on every no-op close.\n\t */\n\tprivate flushAndCloseDb(): Promise<void> {\n\t\t// A persist in flight can't be joined (persistIfNeeded bails while one is running) and,\n\t\t// now that we're disposed, won't schedule a follow-up. Edits queued during that write\n\t\t// would be lost if the db closed now, so wait for it and flush again.\n\t\tif (this.persistPromise) {\n\t\t\treturn this.persistPromise.then(() => this.flushAndCloseDb())\n\t\t}\n\t\tif (this.didLoad && this.diffQueue.length > 0) this.persistIfNeeded()\n\t\t// the db waits for the transaction the flush just opened before actually closing\n\t\treturn this.db.close()\n\t}\n\n\tprivate isPersisting = false\n\tprivate persistPromise: Promise<void> | null = null\n\tprivate didLastWriteError = false\n\t// eslint-disable-next-line no-restricted-globals\n\tprivate scheduledPersistTimeout: ReturnType<typeof setTimeout> | null = null\n\n\t/**\n\t * Schedule a persist. Persists don't happen immediately: they are throttled to avoid writing too\n\t * often, and will retry if failed.\n\t *\n\t * @internal\n\t */\n\tprivate schedulePersist() {\n\t\tthis.debug('schedulePersist', this.scheduledPersistTimeout)\n\t\tif (this.scheduledPersistTimeout) return\n\t\t// A deferred persist would try to write to a database that is closing or already closed.\n\t\tif (this.didDispose) return\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tthis.scheduledPersistTimeout = setTimeout(\n\t\t\t() => {\n\t\t\t\tthis.scheduledPersistTimeout = null\n\t\t\t\tthis.persistIfNeeded()\n\t\t\t},\n\t\t\tthis.didLastWriteError ? PERSIST_RETRY_THROTTLE_MS : PERSIST_THROTTLE_MS\n\t\t)\n\t}\n\n\t/**\n\t * Persist to IndexedDB only under certain circumstances:\n\t *\n\t * - If we're not already persisting\n\t * - If we're not reloading the page\n\t * - And we have something to persist (a full db write scheduled or changes in the diff queue)\n\t *\n\t * @internal\n\t */\n\tprivate persistIfNeeded() {\n\t\tthis.debug('persistIfNeeded', {\n\t\t\tisPersisting: this.isPersisting,\n\t\t\tisReloading: this.isReloading,\n\t\t\tshouldDoFullDBWrite: this.shouldDoFullDBWrite,\n\t\t\tdiffQueueLength: this.diffQueue.length,\n\t\t\tstoreIsPossiblyCorrupt: this.store.isPossiblyCorrupted(),\n\t\t})\n\n\t\t// if we've scheduled a persist for the future, that's no longer needed\n\t\tif (this.scheduledPersistTimeout) {\n\t\t\tclearTimeout(this.scheduledPersistTimeout)\n\t\t\tthis.scheduledPersistTimeout = null\n\t\t}\n\n\t\tif (!this.didLoad) return\n\n\t\t// if a persist is already in progress, we don't need to do anything -\n\t\t// if there are still outstanding changes once it's finished, it'll\n\t\t// schedule another persist\n\t\tif (this.isPersisting) return\n\n\t\t// if we're reloading the page, it's because there's a newer client\n\t\t// present so lets not overwrite their changes\n\t\tif (this.isReloading) return\n\n\t\t// if the store is possibly corrupted, we don't want to persist\n\t\tif (this.store.isPossiblyCorrupted()) return\n\n\t\t// if we're scheduled for a full write or if we have changes outstanding, let's persist them!\n\t\tif (this.shouldDoFullDBWrite || this.diffQueue.length > 0) {\n\t\t\tthis.persistPromise = this.doPersist()\n\t\t}\n\t}\n\n\t/**\n\t * Actually persist to IndexedDB. If the write fails, then we'll retry with a full db write after\n\t * a short delay.\n\t */\n\tprivate async doPersist() {\n\t\tassert(!this.isPersisting, 'persist already in progress')\n\t\tthis.isPersisting = true\n\n\t\tthis.debug('doPersist start')\n\n\t\t// instantly empty the diff queue, but keep our own copy of it. this way\n\t\t// diffs that come in during the persist will still get tracked\n\t\tconst diffQueue = this.diffQueue\n\t\tthis.diffQueue = []\n\n\t\ttry {\n\t\t\tif (this.shouldDoFullDBWrite) {\n\t\t\t\tthis.shouldDoFullDBWrite = false\n\t\t\t\tawait this.db.storeSnapshot({\n\t\t\t\t\tschema: this.store.schema,\n\t\t\t\t\tsnapshot: this.store.serialize(),\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t\tsessionStateSnapshot: this.$sessionStateSnapshot.get(),\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tconst diffs = squashRecordDiffs(\n\t\t\t\t\tdiffQueue.filter((d): d is RecordsDiff<UnknownRecord> => d !== UPDATE_INSTANCE_STATE)\n\t\t\t\t)\n\t\t\t\tawait this.db.storeChanges({\n\t\t\t\t\tchanges: diffs,\n\t\t\t\t\tschema: this.store.schema,\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t\tsessionStateSnapshot: this.$sessionStateSnapshot.get(),\n\t\t\t\t})\n\t\t\t}\n\t\t\tthis.didLastWriteError = false\n\t\t} catch (e) {\n\t\t\t// set this.shouldDoFullDBWrite because we clear the diffQueue no matter what,\n\t\t\t// so if this is just a temporary error, we will still persist all changes\n\t\t\tthis.shouldDoFullDBWrite = true\n\t\t\tthis.didLastWriteError = true\n\t\t\tconsole.error('failed to store changes in indexed db', e)\n\n\t\t\tif (!this.didDispose && typeof window !== 'undefined') {\n\t\t\t\tshowCantWriteToIndexDbAlert()\n\t\t\t\trefreshPage()\n\t\t\t}\n\t\t}\n\n\t\tthis.isPersisting = false\n\t\tthis.persistPromise = null\n\t\tthis.debug('doPersist end')\n\n\t\t// changes might have come in between when we started the persist and\n\t\t// now. we request another persist so any new changes can get written\n\t\tthis.schedulePersist()\n\t}\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAiC;AACjC,mBAAgF;AAEhF,mBAA6B;AAC7B,oCAMO;AACP,qBAA4B;AAC5B,oBAA0E;AAC1E,4BAA+B;AAG/B,MAAM,sBAAsB;AAE5B,MAAM,4BAA4B;AAElC,MAAM,wBAAwB,uBAAO,uBAAuB;AA4B5D,MAAM,MAAM,CAACA,SAAiBA;AAGvB,MAAM,qBAAqB;AAAA,EACjC;AAAA,EACA,YAAY,OAAe;AAAA,EAE3B;AAAA,EACA,YAAY,MAAe;AAAA,EAE3B;AAAA,EACA,QAAQ;AAAA,EAER;AACD;AAEA,MAAM,KAAK,OAAO,qBAAqB,cAAc,uBAAuB;AAG5E,MAAM,iBAAiB,oBAAI,IAA2B;AAG/C,MAAM,kBAAkB;AAAA,EAuB9B,YACiB,OAChB;AAAA,IACC;AAAA,IACA,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,EACD,GAMgB,UAAU,IAAI,GAAG,mBAAmB,cAAc,EAAE,GACnE;AAbe;AAYA;AAEhB,QAAI,OAAO,WAAW,aAAa;AAClC;AAAC,MAAC,OAAe,SAAS;AAAA,IAC3B;AACA,SAAK,iBAAiB;AACtB,SAAK,YAAY;AACjB,SAAK,KAAK,IAAI,qCAAe,cAAc;AAE3C,SAAK,mBAAmB,KAAK,MAAM,OAAO,UAAU;AACpD,SAAK,4BAAwB,gEAAiC,KAAK,KAAK;AAExE,SAAK,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA,MAKhB,MAAM;AAAA,QACL,CAAC,EAAE,QAAQ,MAAM;AAChB,eAAK,UAAU,KAAK,OAAO;AAC3B,eAAK,QAAQ;AAAA,YACZ,IAAI;AAAA,cACH,MAAM;AAAA,cACN,SAAS,KAAK,MAAM;AAAA,cACpB;AAAA,cACA,QAAQ,KAAK;AAAA,YACd,CAAC;AAAA,UACF;AACA,eAAK,gBAAgB;AAAA,QACtB;AAAA,QACA,EAAE,QAAQ,QAAQ,OAAO,WAAW;AAAA,MACrC;AAAA,IACD;AACA,SAAK,YAAY;AAAA,MAChB,MAAM;AAAA,QACL,MAAM;AACL,eAAK,UAAU,KAAK,qBAAqB;AACzC,eAAK,gBAAgB;AAAA,QACtB;AAAA,QACA,EAAE,OAAO,UAAU;AAAA,MACpB;AAAA,IACD;AAEA,QAAI,OAAO,WAAW,aAAa;AAKlC,YAAM,QAAQ,MAAM;AAGnB,YAAI,CAAC,KAAK,QAAS;AACnB,aAAK,gBAAgB;AAAA,MACtB;AACA,YAAM,qBAAqB,MAAM;AAChC,YAAI,SAAS,oBAAoB,SAAU,OAAM;AAAA,MAClD;AACA,aAAO,iBAAiB,YAAY,KAAK;AACzC,eAAS,iBAAiB,oBAAoB,kBAAkB;AAChE,WAAK,YAAY,IAAI,MAAM;AAC1B,eAAO,oBAAoB,YAAY,KAAK;AAC5C,iBAAS,oBAAoB,oBAAoB,kBAAkB;AAAA,MACpE,CAAC;AAAA,IACF;AAEA,SAAK,QAAQ,QAAQ,WAAW;AAEhC,SAAK,gBAAgB,IAAI;AAAA,MACxB,OAAO,OAAO,KAAK,MAAM,OAAO,KAAK,EACnC,OAAO,CAAC,MAAM,EAAE,UAAU,UAAU,EACpC,IAAI,CAAC,MAAM,EAAE,QAAQ;AAAA,IACxB;AAAA,EACD;AAAA,EApFiB;AAAA,EAYA;AAAA,EAnCT,cAAc,oBAAI,IAAgB;AAAA,EAClC,YAA8E,CAAC;AAAA,EAC/E,aAAa;AAAA,EACb,UAAU;AAAA,EACV,sBAAsB;AAAA,EACtB,cAAc;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACD,cAAc;AAAA,EACL;AAAA,EACA;AAAA;AAAA,EAER;AAAA,EAET,WAAW,KAAK,IAAI;AAAA,EACZ,SAAS,MAAa;AAC7B,QAAI,KAAK,aAAa;AAErB,cAAQ,MAAM,GAAG,IAAI;AAAA,IACtB;AAAA,EACD;AAAA,EAwFA,MAAc,QAAQ,QAAgC,aAAqC;AAC1F,SAAK,MAAM,YAAY;AACvB,QAAI;AAEJ,UAAM,gBAAgB,CAACA,SAAiB;AAGvC,YAAM,MAAM,KAAK,MAAM,OAAO,mBAAmBA,KAAI,MAAM;AAE3D,UAAI,CAAC,IAAI,IAAI;AACZ,aAAK,cAAc;AAGnB,cAAM,gBAAgB,KAAK,IAAI,IAAI,KAAK;AACxC,YAAI,gBAAgB,KAAM;AAMzB,sBAAY,IAAI,MAAM,8DAA8D,CAAC;AACrF,iBAAO;AAAA,QACR;AACA,aAAK,MAAM,WAAW;AACtB,YAAI,OAAO,WAAW,YAAa,iCAAY;AAC/C,eAAO;AAAA,MACR,WAAW,IAAI,MAAM,SAAS,GAAG;AAEhC,aAAK,MAAM,wBAAwB;AACnC,aAAK,QAAQ,YAAY,EAAE,MAAM,YAAY,QAAQ,KAAK,iBAAiB,CAAC;AAE5E,aAAK,sBAAsB;AAC3B,aAAK,gBAAgB;AACrB,eAAO;AAAA,MACR;AAEA,UAAIA,KAAI,SAAS,QAAQ;AACxB,aAAK,MAAM,eAAe;AAC1B,mCAAS,MAAM;AACd,eAAK,MAAM,mBAAmB,MAAM;AACnC,iBAAK,MAAM,UAAUA,KAAI,OAAc;AAAA,UACxC,CAAC;AAAA,QACF,CAAC;AAAA,MACF;AACA,aAAO;AAAA,IACR;AAKA,QAAI,mBAAqC,CAAC;AAC1C,SAAK,QAAQ,YAAY,CAAC,EAAE,MAAAC,MAAK,MAAM;AACtC,WAAK,MAAM,eAAeA,KAAI;AAC9B,UAAI,kBAAkB;AACrB,yBAAiB,KAAKA,KAAe;AAAA,MACtC,OAAO;AACN,sBAAcA,KAAe;AAAA,MAC9B;AAAA,IACD;AACA,SAAK,YAAY,IAAI,MAAM;AAC1B,WAAK,QAAQ,MAAM;AAAA,IACpB,CAAC;AAID,UAAM,eAAe,IAAI,KAAK,cAAc;AAC5C,QAAI,KAAK,WAAY;AAErB,QAAI;AACH,aAAO,MAAM,KAAK,GAAG,KAAK,EAAE,WAAW,KAAK,UAAU,CAAC;AAAA,IACxD,SAAS,OAAY;AACpB,kBAAY,KAAK;AACjB,sDAA6B;AAC7B;AAAA,IACD;AAEA,SAAK,MAAM,0BAA0B,MAAM,cAAc,KAAK,UAAU;AACxE,QAAI,KAAK,WAAY;AAErB,QAAI;AACH,UAAI,MAAM;AACT,cAAM,mBAAmB,OAAO,YAAY,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AAC9E,cAAM,uBACL,KAAK,4BAAwB,qEAAsC,gBAAgB;AACpF,cAAM,kBAAkB,KAAK,MAAM,OAAO,qBAAqB;AAAA,UAC9D,OAAO;AAAA;AAAA,UAEP,QAAQ,KAAK,UAAU,KAAK,MAAM,OAAO,yBAAyB;AAAA,QACnE,CAAC;AAED,YAAI,gBAAgB,SAAS,SAAS;AACrC,kBAAQ,MAAM,2BAA2B,eAAe;AACxD,sBAAY,IAAI,MAAM,4BAA4B,gBAAgB,MAAM,EAAE,CAAC;AAC3E;AAAA,QACD;AAEA,cAAM,UAAU,OAAO,OAAO,gBAAgB,KAAK,EAAE;AAAA,UAAO,CAAC,MAC5D,KAAK,cAAc,IAAI,EAAE,QAAQ;AAAA,QAClC;AACA,YAAI,QAAQ,SAAS,GAAG;AAEvB,eAAK,MAAM,mBAAmB,MAAM;AAEnC,iBAAK,MAAM,IAAI,SAAS,YAAY;AAAA,UACrC,CAAC;AAAA,QACF;AAEA,YAAI,sBAAsB;AACzB,+EAAkC,KAAK,OAAO,sBAAsB;AAAA,YACnE,gBAAgB;AAAA,UACjB,CAAC;AAAA,QACF;AAAA,MACD;AAGA,YAAM,WAAW;AACjB,yBAAmB;AACnB,iBAAW,WAAW,UAAU;AAC/B,YAAI,CAAC,cAAc,OAAO,EAAG;AAAA,MAC9B;AACA,WAAK,UAAU;AACf,UAAI,KAAK,UAAU,SAAS,EAAG,MAAK,gBAAgB;AAEpD,WAAK,QAAQ,YAAY,EAAE,MAAM,YAAY,QAAQ,KAAK,iBAAiB,CAAC;AAC5E,aAAO,IAAI;AAIX,WAAK,GAAG,cAAc,EAAE,eAAe,KAAK,UAAU,CAAC,EAAE,MAAM,CAAC,MAAe;AAC9E,aAAK,MAAM,sCAAsC,CAAC;AAAA,MACnD,CAAC;AAAA,IACF,SAAS,GAAQ;AAChB,WAAK,MAAM,iCAAiC,CAAC;AAC7C,UAAI,KAAK,WAAY;AACrB,kBAAY,CAAC;AACb;AAAA,IACD;AAAA,EACD;AAAA,EAEA,QAAQ;AACP,QAAI,KAAK,WAAY;AACrB,SAAK,MAAM,SAAS;AACpB,SAAK,aAAa;AAClB,SAAK,YAAY,QAAQ,CAAC,MAAM,EAAE,CAAC;AACnC,QAAI,KAAK,yBAAyB;AACjC,mBAAa,KAAK,uBAAuB;AACzC,WAAK,0BAA0B;AAAA,IAChC;AACA,QAAI,OAAO,WAAW,eAAgB,OAAe,WAAW,MAAM;AACrE,aAAQ,OAAe;AAAA,IACxB;AAIA,UAAM,WAAW,eAAe,IAAI,KAAK,cAAc;AACvD,UAAM,SAAS,WAAW,SAAS,KAAK,MAAM,KAAK,gBAAgB,CAAC,IAAI,KAAK,gBAAgB,GAC3F,MAAM,iBAAI,EACV,KAAK,MAAM;AACX,UAAI,eAAe,IAAI,KAAK,cAAc,MAAM,OAAO;AACtD,uBAAe,OAAO,KAAK,cAAc;AAAA,MAC1C;AAAA,IACD,CAAC;AACF,mBAAe,IAAI,KAAK,gBAAgB,KAAK;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,kBAAiC;AAIxC,QAAI,KAAK,gBAAgB;AACxB,aAAO,KAAK,eAAe,KAAK,MAAM,KAAK,gBAAgB,CAAC;AAAA,IAC7D;AACA,QAAI,KAAK,WAAW,KAAK,UAAU,SAAS,EAAG,MAAK,gBAAgB;AAEpE,WAAO,KAAK,GAAG,MAAM;AAAA,EACtB;AAAA,EAEQ,eAAe;AAAA,EACf,iBAAuC;AAAA,EACvC,oBAAoB;AAAA;AAAA,EAEpB,0BAAgE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQhE,kBAAkB;AACzB,SAAK,MAAM,mBAAmB,KAAK,uBAAuB;AAC1D,QAAI,KAAK,wBAAyB;AAElC,QAAI,KAAK,WAAY;AAErB,SAAK,0BAA0B;AAAA,MAC9B,MAAM;AACL,aAAK,0BAA0B;AAC/B,aAAK,gBAAgB;AAAA,MACtB;AAAA,MACA,KAAK,oBAAoB,4BAA4B;AAAA,IACtD;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,kBAAkB;AACzB,SAAK,MAAM,mBAAmB;AAAA,MAC7B,cAAc,KAAK;AAAA,MACnB,aAAa,KAAK;AAAA,MAClB,qBAAqB,KAAK;AAAA,MAC1B,iBAAiB,KAAK,UAAU;AAAA,MAChC,wBAAwB,KAAK,MAAM,oBAAoB;AAAA,IACxD,CAAC;AAGD,QAAI,KAAK,yBAAyB;AACjC,mBAAa,KAAK,uBAAuB;AACzC,WAAK,0BAA0B;AAAA,IAChC;AAEA,QAAI,CAAC,KAAK,QAAS;AAKnB,QAAI,KAAK,aAAc;AAIvB,QAAI,KAAK,YAAa;AAGtB,QAAI,KAAK,MAAM,oBAAoB,EAAG;AAGtC,QAAI,KAAK,uBAAuB,KAAK,UAAU,SAAS,GAAG;AAC1D,WAAK,iBAAiB,KAAK,UAAU;AAAA,IACtC;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,YAAY;AACzB,6BAAO,CAAC,KAAK,cAAc,6BAA6B;AACxD,SAAK,eAAe;AAEpB,SAAK,MAAM,iBAAiB;AAI5B,UAAM,YAAY,KAAK;AACvB,SAAK,YAAY,CAAC;AAElB,QAAI;AACH,UAAI,KAAK,qBAAqB;AAC7B,aAAK,sBAAsB;AAC3B,cAAM,KAAK,GAAG,cAAc;AAAA,UAC3B,QAAQ,KAAK,MAAM;AAAA,UACnB,UAAU,KAAK,MAAM,UAAU;AAAA,UAC/B,WAAW,KAAK;AAAA,UAChB,sBAAsB,KAAK,sBAAsB,IAAI;AAAA,QACtD,CAAC;AAAA,MACF,OAAO;AACN,cAAM,YAAQ;AAAA,UACb,UAAU,OAAO,CAAC,MAAuC,MAAM,qBAAqB;AAAA,QACrF;AACA,cAAM,KAAK,GAAG,aAAa;AAAA,UAC1B,SAAS;AAAA,UACT,QAAQ,KAAK,MAAM;AAAA,UACnB,WAAW,KAAK;AAAA,UAChB,sBAAsB,KAAK,sBAAsB,IAAI;AAAA,QACtD,CAAC;AAAA,MACF;AACA,WAAK,oBAAoB;AAAA,IAC1B,SAAS,GAAG;AAGX,WAAK,sBAAsB;AAC3B,WAAK,oBAAoB;AACzB,cAAQ,MAAM,yCAAyC,CAAC;AAExD,UAAI,CAAC,KAAK,cAAc,OAAO,WAAW,aAAa;AACtD,uDAA4B;AAC5B,wCAAY;AAAA,MACb;AAAA,IACD;AAEA,SAAK,eAAe;AACpB,SAAK,iBAAiB;AACtB,SAAK,MAAM,eAAe;AAI1B,SAAK,gBAAgB;AAAA,EACtB;AACD;",
|
|
6
6
|
"names": ["msg", "data"]
|
|
7
7
|
}
|
package/dist-cjs/version.js
CHANGED
|
@@ -22,10 +22,10 @@ __export(version_exports, {
|
|
|
22
22
|
version: () => version
|
|
23
23
|
});
|
|
24
24
|
module.exports = __toCommonJS(version_exports);
|
|
25
|
-
const version = "5.
|
|
25
|
+
const version = "5.6.0-canary.669b4c97f643";
|
|
26
26
|
const publishDates = {
|
|
27
27
|
major: "2026-05-06T16:28:18.473Z",
|
|
28
|
-
minor: "2026-09-
|
|
29
|
-
patch: "2026-09-
|
|
28
|
+
minor: "2026-09-30T12:35:27.842Z",
|
|
29
|
+
patch: "2026-09-30T12:35:27.842Z"
|
|
30
30
|
};
|
|
31
31
|
//# sourceMappingURL=version.js.map
|
package/dist-cjs/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/version.ts"],
|
|
4
|
-
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.
|
|
4
|
+
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.6.0-canary.669b4c97f643'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-09-30T12:35:27.842Z',\n\tpatch: '2026-09-30T12:35:27.842Z',\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/index.mjs
CHANGED
|
@@ -312,7 +312,7 @@ import { LocalIndexedDb, Table } from "./lib/utils/sync/LocalIndexedDb.mjs";
|
|
|
312
312
|
import { uniq } from "./lib/utils/uniq.mjs";
|
|
313
313
|
registerTldrawLibraryVersion(
|
|
314
314
|
"@tldraw/editor",
|
|
315
|
-
"5.
|
|
315
|
+
"5.6.0-canary.669b4c97f643",
|
|
316
316
|
"esm"
|
|
317
317
|
);
|
|
318
318
|
import { getColorValue } from "./lib/editor/managers/ThemeManager/defaultThemes.mjs";
|
|
@@ -99,7 +99,8 @@ import {
|
|
|
99
99
|
approximately,
|
|
100
100
|
areAnglesCompatible,
|
|
101
101
|
clamp,
|
|
102
|
-
pointInPolygon
|
|
102
|
+
pointInPolygon,
|
|
103
|
+
shortAngleDist
|
|
103
104
|
} from "../primitives/utils.mjs";
|
|
104
105
|
import { Vec } from "../primitives/Vec.mjs";
|
|
105
106
|
import { areShapesContentEqual } from "../utils/areShapesContentEqual.mjs";
|
|
@@ -3698,7 +3699,9 @@ class Editor extends EventEmitter {
|
|
|
3698
3699
|
const id = typeof page === "string" ? page : page.id;
|
|
3699
3700
|
const freshPage = this.getPage(id);
|
|
3700
3701
|
if (!freshPage) return this;
|
|
3701
|
-
const prevCamera = {
|
|
3702
|
+
const prevCamera = {
|
|
3703
|
+
...(this.store.get(CameraRecordType.createId(freshPage.id)) ?? this.getCamera())
|
|
3704
|
+
};
|
|
3702
3705
|
const content = this.getContentFromCurrentPage(this.getSortedChildIdsForParent(freshPage.id));
|
|
3703
3706
|
this.run(() => {
|
|
3704
3707
|
const pages = this.getPages();
|
|
@@ -3707,7 +3710,7 @@ class Editor extends EventEmitter {
|
|
|
3707
3710
|
this.setCurrentPage(createId);
|
|
3708
3711
|
this.setCamera(prevCamera);
|
|
3709
3712
|
if (content) {
|
|
3710
|
-
return this.putContentOntoCurrentPage(content);
|
|
3713
|
+
return this.putContentOntoCurrentPage(content, { preservePosition: true });
|
|
3711
3714
|
}
|
|
3712
3715
|
});
|
|
3713
3716
|
return this;
|
|
@@ -6071,11 +6074,20 @@ class Editor extends EventEmitter {
|
|
|
6071
6074
|
initialShape: options.initialShape,
|
|
6072
6075
|
initialBounds: options.initialBounds,
|
|
6073
6076
|
isAspectRatioLocked: options.isAspectRatioLocked,
|
|
6074
|
-
initialPageTransform: options.initialPageTransform
|
|
6077
|
+
initialPageTransform: options.initialPageTransform,
|
|
6078
|
+
dragHandle: options.dragHandle,
|
|
6079
|
+
mode: options.mode,
|
|
6080
|
+
// this is one frame of the same resize, not a new one, so don't fire start/end again
|
|
6081
|
+
skipStartAndEndCallbacks: options.skipStartAndEndCallbacks
|
|
6075
6082
|
});
|
|
6076
6083
|
if (Math.sign(scale.x) * Math.sign(scale.y) < 0) {
|
|
6077
6084
|
const parentRotation = this.getShapeParentTransform(id).rotation();
|
|
6078
|
-
const
|
|
6085
|
+
const parentDelta = shortAngleDist(
|
|
6086
|
+
Mat.Cast(options.initialPageTransform).rotation() - options.initialShape.rotation,
|
|
6087
|
+
parentRotation
|
|
6088
|
+
);
|
|
6089
|
+
const initialParentRotation = parentRotation - parentDelta;
|
|
6090
|
+
const rotation = 2 * options.scaleAxisRotation - options.initialShape.rotation - 2 * initialParentRotation - parentDelta;
|
|
6079
6091
|
this.updateShapes([{ id, type, rotation }]);
|
|
6080
6092
|
}
|
|
6081
6093
|
const preScaleShapePageCenter = Mat.applyToPoint(
|