@tldraw/editor 5.5.2 → 5.6.0-canary.0091c91994d6

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.
Files changed (107) hide show
  1. package/dist-cjs/index.d.ts +167 -2
  2. package/dist-cjs/index.js +7 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +18 -5
  5. package/dist-cjs/lib/TldrawEditor.js.map +3 -3
  6. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +57 -15
  7. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
  8. package/dist-cjs/lib/config/TLI18n.js +64 -0
  9. package/dist-cjs/lib/config/TLI18n.js.map +7 -0
  10. package/dist-cjs/lib/editor/Editor.js +44 -16
  11. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  12. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +10 -3
  13. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
  14. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +1 -1
  15. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  16. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +19 -2
  17. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  18. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +3 -5
  19. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
  20. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +7 -1
  21. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +2 -2
  22. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +28 -0
  23. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  24. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +3 -0
  25. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
  26. package/dist-cjs/lib/editor/shapes/shared/resizeBox.js +21 -16
  27. package/dist-cjs/lib/editor/shapes/shared/resizeBox.js.map +2 -2
  28. package/dist-cjs/lib/hooks/useCursor.js +12 -8
  29. package/dist-cjs/lib/hooks/useCursor.js.map +2 -2
  30. package/dist-cjs/lib/hooks/useDocumentEvents.js +28 -3
  31. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  32. package/dist-cjs/lib/license/LicenseManager.js +64 -1
  33. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  34. package/dist-cjs/lib/options.js +1 -1
  35. package/dist-cjs/lib/options.js.map +1 -1
  36. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +12 -5
  37. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  38. package/dist-cjs/version.js +3 -3
  39. package/dist-cjs/version.js.map +1 -1
  40. package/dist-esm/index.d.mts +167 -2
  41. package/dist-esm/index.mjs +13 -1
  42. package/dist-esm/index.mjs.map +2 -2
  43. package/dist-esm/lib/TldrawEditor.mjs +18 -5
  44. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  45. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +61 -16
  46. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
  47. package/dist-esm/lib/config/TLI18n.mjs +44 -0
  48. package/dist-esm/lib/config/TLI18n.mjs.map +7 -0
  49. package/dist-esm/lib/editor/Editor.mjs +46 -17
  50. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  51. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +10 -3
  52. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
  53. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +1 -1
  54. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  55. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +20 -3
  56. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  57. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +3 -5
  58. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
  59. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +7 -1
  60. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +2 -2
  61. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +28 -0
  62. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  63. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +3 -0
  64. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
  65. package/dist-esm/lib/editor/shapes/shared/resizeBox.mjs +21 -16
  66. package/dist-esm/lib/editor/shapes/shared/resizeBox.mjs.map +2 -2
  67. package/dist-esm/lib/hooks/useCursor.mjs +12 -8
  68. package/dist-esm/lib/hooks/useCursor.mjs.map +2 -2
  69. package/dist-esm/lib/hooks/useDocumentEvents.mjs +28 -3
  70. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  71. package/dist-esm/lib/license/LicenseManager.mjs +64 -1
  72. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  73. package/dist-esm/lib/options.mjs +1 -1
  74. package/dist-esm/lib/options.mjs.map +1 -1
  75. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +13 -6
  76. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  77. package/dist-esm/version.mjs +3 -3
  78. package/dist-esm/version.mjs.map +1 -1
  79. package/editor.css +1 -1
  80. package/package.json +17 -19
  81. package/src/index.ts +14 -2
  82. package/src/lib/TldrawEditor.tsx +44 -4
  83. package/src/lib/components/default-components/DefaultErrorFallback.tsx +66 -18
  84. package/src/lib/config/TLI18n.test.ts +84 -0
  85. package/src/lib/config/TLI18n.ts +172 -0
  86. package/src/lib/editor/Editor.ts +90 -33
  87. package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +19 -3
  88. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +17 -0
  89. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +2 -2
  90. package/src/lib/editor/managers/FontManager/FontManager.test.ts +1 -0
  91. package/src/lib/editor/managers/FontManager/FontManager.ts +26 -3
  92. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +32 -0
  93. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +4 -6
  94. package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +16 -7
  95. package/src/lib/editor/shapes/ShapeUtil.ts +30 -0
  96. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +4 -0
  97. package/src/lib/editor/shapes/shared/resizeBox.ts +21 -16
  98. package/src/lib/hooks/useCursor.ts +14 -7
  99. package/src/lib/hooks/useDocumentEvents.ts +51 -3
  100. package/src/lib/license/LicenseManager.test.ts +220 -0
  101. package/src/lib/license/LicenseManager.ts +89 -1
  102. package/src/lib/options.ts +1 -1
  103. package/src/lib/test/useCursor.test.tsx +36 -0
  104. package/src/lib/test/useDocumentEvents.test.tsx +153 -22
  105. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +73 -0
  106. package/src/lib/utils/sync/TLLocalSyncClient.ts +24 -8
  107. package/src/version.ts +3 -3
@@ -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": "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",
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": "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",
6
6
  "names": ["msg", "data"]
7
7
  }
@@ -1,8 +1,8 @@
1
- const version = "5.5.2";
1
+ const version = "5.6.0-canary.0091c91994d6";
2
2
  const publishDates = {
3
3
  major: "2026-05-06T16:28:18.473Z",
4
- minor: "2026-09-30T10:03:27.724Z",
5
- patch: "2026-10-02T13:35:40.683Z"
4
+ minor: "2026-10-28T11:15:20.344Z",
5
+ patch: "2026-10-28T11:15:20.344Z"
6
6
  };
7
7
  export {
8
8
  publishDates,
@@ -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.5.2'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-09-30T10:03:27.724Z',\n\tpatch: '2026-10-02T13:35:40.683Z',\n}\n"],
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.0091c91994d6'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-10-28T11:15:20.344Z',\n\tpatch: '2026-10-28T11:15:20.344Z',\n}\n"],
5
5
  "mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
6
6
  "names": []
7
7
  }
package/editor.css CHANGED
@@ -82,7 +82,7 @@
82
82
  /* Hotspot sits between the white outline tip and the black tip: on either tip alone, hit-testing feels offset from one direction (#10855) */
83
83
  --tl-cursor-default:
84
84
  url("data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z' fill='white'/><path d='m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z' fill='white'/><path d='m19.751 24.4155-1.844.774-3.1-7.374 1.841-.775z' fill='black'/><path d='m13 10.814v11.188l2.969-2.866.428-.139h4.768z' fill='black'/></g></svg>")
85
- 12 10,
85
+ 12 9,
86
86
  default;
87
87
  --tl-cursor-pointer:
88
88
  url("data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m13.3315 21.3799c-.284-.359-.629-1.093-1.243-1.984-.348-.504-1.211-1.453-1.468-1.935-.223-.426-.199-.617-.146-.97.094-.628.738-1.117 1.425-1.051.519.049.959.392 1.355.716.239.195.533.574.71.788.163.196.203.277.377.509.23.307.302.459.214.121-.071-.496-.187-1.343-.355-2.092-.128-.568-.159-.657-.281-1.093-.129-.464-.195-.789-.316-1.281-.084-.348-.235-1.059-.276-1.459-.057-.547-.087-1.439.264-1.849.275-.321.906-.418 1.297-.22.512.259.803 1.003.936 1.3.239.534.387 1.151.516 1.961.164 1.031.466 2.462.476 2.763.024-.369-.068-1.146-.004-1.5.058-.321.328-.694.666-.795.286-.085.621-.116.916-.055.313.064.643.288.766.499.362.624.369 1.899.384 1.831.086-.376.071-1.229.284-1.584.14-.234.497-.445.687-.479.294-.052.655-.068.964-.008.249.049.586.345.677.487.218.344.342 1.317.379 1.658.015.141.074-.392.293-.736.406-.639 1.843-.763 1.898.639.025.654.02.624.02 1.064 0 .517-.012.828-.04 1.202-.031.4-.117 1.304-.242 1.742-.086.301-.371.978-.652 1.384 0 0-1.074 1.25-1.191 1.813-.118.562-.079.566-.102.965-.023.398.121.922.121.922s-.802.104-1.234.035c-.391-.063-.875-.841-1-1.079-.172-.328-.539-.265-.682-.023-.225.383-.709 1.07-1.051 1.113-.668.084-2.054.031-3.139.02 0 0 .185-1.011-.227-1.358-.305-.259-.83-.784-1.144-1.06z' fill='white'/><g stroke='black' stroke-linecap='round' stroke-width='.75'><path d='m13.3315 21.3799c-.284-.359-.629-1.093-1.243-1.984-.348-.504-1.211-1.453-1.468-1.935-.223-.426-.199-.617-.146-.97.094-.628.738-1.117 1.425-1.051.519.049.959.392 1.355.716.239.195.533.574.71.788.163.196.203.277.377.509.23.307.302.459.214.121-.071-.496-.187-1.343-.355-2.092-.128-.568-.159-.657-.281-1.093-.129-.464-.195-.789-.316-1.281-.084-.348-.235-1.059-.276-1.459-.057-.547-.087-1.439.264-1.849.275-.321.906-.418 1.297-.22.512.259.803 1.003.936 1.3.239.534.387 1.151.516 1.961.164 1.031.466 2.462.476 2.763.024-.369-.068-1.146-.004-1.5.058-.321.328-.694.666-.795.286-.085.621-.116.916-.055.313.064.643.288.766.499.362.624.369 1.899.384 1.831.086-.376.071-1.229.284-1.584.14-.234.497-.445.687-.479.294-.052.655-.068.964-.008.249.049.586.345.677.487.218.344.342 1.317.379 1.658.015.141.074-.392.293-.736.406-.639 1.843-.763 1.898.639.025.654.02.624.02 1.064 0 .517-.012.828-.04 1.202-.031.4-.117 1.304-.242 1.742-.086.301-.371.978-.652 1.384 0 0-1.074 1.25-1.191 1.813-.118.562-.079.566-.102.965-.023.398.121.922.121.922s-.802.104-1.234.035c-.391-.063-.875-.841-1-1.079-.172-.328-.539-.265-.682-.023-.225.383-.709 1.07-1.051 1.113-.668.084-2.054.031-3.139.02 0 0 .185-1.011-.227-1.358-.305-.259-.83-.784-1.144-1.06z' stroke-linejoin='round'/><path d='m21.5664 21.7344v-3.459'/><path d='m19.5508 21.7461-.016-3.473'/><path d='m17.5547 18.3047.021 3.426'/></g></g></svg>")
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/editor",
3
3
  "description": "tldraw infinite canvas SDK (editor).",
4
- "version": "5.5.2",
4
+ "version": "5.6.0-canary.0091c91994d6",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -39,27 +39,16 @@
39
39
  "dist-cjs",
40
40
  "src"
41
41
  ],
42
- "scripts": {
43
- "test-ci": "yarn run -T vitest run --passWithNoTests",
44
- "test": "yarn run -T vitest --passWithNoTests",
45
- "test-coverage": "yarn run -T vitest run --coverage --passWithNoTests",
46
- "build": "yarn run -T tsx ../../internal/scripts/build-package.ts",
47
- "build-api": "yarn run -T tsx ../../internal/scripts/build-api.ts",
48
- "prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
49
- "postpack": "../../internal/scripts/postpack.sh",
50
- "pack-tarball": "yarn pack",
51
- "lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
52
- },
53
42
  "dependencies": {
54
43
  "@tiptap/core": "^3.12.1",
55
44
  "@tiptap/pm": "^3.12.1",
56
45
  "@tiptap/react": "^3.12.1",
57
- "@tldraw/state": "5.5.2",
58
- "@tldraw/state-react": "5.5.2",
59
- "@tldraw/store": "5.5.2",
60
- "@tldraw/tlschema": "5.5.2",
61
- "@tldraw/utils": "5.5.2",
62
- "@tldraw/validate": "5.5.2",
46
+ "@tldraw/state": "5.6.0-canary.0091c91994d6",
47
+ "@tldraw/state-react": "5.6.0-canary.0091c91994d6",
48
+ "@tldraw/store": "5.6.0-canary.0091c91994d6",
49
+ "@tldraw/tlschema": "5.6.0-canary.0091c91994d6",
50
+ "@tldraw/utils": "5.6.0-canary.0091c91994d6",
51
+ "@tldraw/validate": "5.6.0-canary.0091c91994d6",
63
52
  "classnames": "^2.5.1",
64
53
  "eventemitter3": "^4.0.7",
65
54
  "idb": "^7.1.1",
@@ -79,7 +68,6 @@
79
68
  "@types/react-dom": "^19.2.3",
80
69
  "@types/wicg-file-system-access": "^2020.9.8",
81
70
  "fake-indexeddb": "^4.0.2",
82
- "lazyrepo": "0.0.0-alpha.27",
83
71
  "react": "^19.2.1",
84
72
  "react-dom": "^19.2.1",
85
73
  "resize-observer-polyfill": "^1.5.1",
@@ -96,5 +84,15 @@
96
84
  "require": "./dist-cjs/index.js"
97
85
  },
98
86
  "./editor.css": "./editor.css"
87
+ },
88
+ "scripts": {
89
+ "test-ci": "pnpm exec vitest run --passWithNoTests",
90
+ "test": "pnpm exec vitest --passWithNoTests",
91
+ "test-coverage": "pnpm exec vitest run --coverage --passWithNoTests",
92
+ "build": "pnpm exec tsx ../../internal/scripts/build-package.ts",
93
+ "build-api": "pnpm exec tsx ../../internal/scripts/build-api.ts",
94
+ "pack-tarball": "pnpm pack",
95
+ "lint": "pnpm exec tsx ../../internal/scripts/lint.ts",
96
+ "build-types": "tsc --build"
99
97
  }
100
98
  }
package/src/index.ts CHANGED
@@ -75,6 +75,17 @@ export {
75
75
  type TLEditorSnapshot,
76
76
  type TLLoadSnapshotOptions,
77
77
  } from './lib/config/TLEditorSnapshot'
78
+ export {
79
+ defaultI18n,
80
+ defineMessages,
81
+ getDeclaredMessages,
82
+ translateMessage,
83
+ type TLI18n,
84
+ type TLI18nAdapter,
85
+ type TLI18nMessage,
86
+ type TLI18nMessageInput,
87
+ type TLI18nValues,
88
+ } from './lib/config/TLI18n'
78
89
  export {
79
90
  createSessionStateSnapshotSignal,
80
91
  extractSessionStateFromLegacySnapshot,
@@ -439,17 +450,18 @@ export {
439
450
  } from './lib/primitives/utils'
440
451
  export { Vec, type VecLike } from './lib/primitives/Vec'
441
452
  export {
453
+ EditorI18nFallbackContext,
442
454
  ErrorScreen,
443
455
  LoadingScreen,
444
- TldrawEditor,
445
- useOnMount,
446
456
  type LoadingScreenProps,
457
+ TldrawEditor,
447
458
  type TldrawEditorBaseProps,
448
459
  type TldrawEditorProps,
449
460
  type TldrawEditorStoreProps,
450
461
  type TldrawEditorWithoutStoreProps,
451
462
  type TldrawEditorWithStoreProps,
452
463
  type TLOnMountHandler,
464
+ useOnMount,
453
465
  } from './lib/TldrawEditor'
454
466
  export { dataUrlToFile, getDefaultCdnBaseUrl } from './lib/utils/assets'
455
467
  export { clampToBrowserMaxCanvasSize, type CanvasMaxSize } from './lib/utils/browserCanvasMaxSize'
@@ -32,6 +32,7 @@ import { TLAnyAssetUtilConstructor } from './config/defaultAssets'
32
32
  import { TLAnyBindingUtilConstructor } from './config/defaultBindings'
33
33
  import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
34
34
  import { TLEditorSnapshot } from './config/TLEditorSnapshot'
35
+ import { defaultI18n, TLI18nAdapter } from './config/TLI18n'
35
36
  import { Editor } from './editor/Editor'
36
37
  import { resolveThemes } from './editor/managers/ThemeManager/ThemeManager'
37
38
  import { TLAnyOverlayUtilConstructor } from './editor/overlays/OverlayUtil'
@@ -253,6 +254,11 @@ export interface TldrawEditorBaseProps {
253
254
  * The URLs for the fonts to use in the editor.
254
255
  */
255
256
  assetUrls?: { fonts?: { [key: string]: string | undefined } }
257
+
258
+ /**
259
+ * An i18n adapter for translating strings.
260
+ */
261
+ i18n?: TLI18nAdapter
256
262
  }
257
263
 
258
264
  /**
@@ -484,6 +490,23 @@ const TldrawEditorWithLoadingStore = memo(function TldrawEditorBeforeLoading({
484
490
 
485
491
  const noAutoFocus = () => getGlobalDocument().location.search.includes('tldraw_preserve_focus')
486
492
 
493
+ /**
494
+ * A slot the UI fills with its own i18n, for an editor whose host passed none. Every app that
495
+ * composes `<TldrawEditor>` and `<TldrawUi>` by hand rather than rendering `<Tldraw>` is one: the
496
+ * `i18n` prop is new, so none of them pass it, and their shape names — which shapes now resolve
497
+ * through the editor's i18n — would have been announced in English. The UI renders inside the
498
+ * editor in every composition, so it can hand its translations up. An `i18n` prop still wins.
499
+ *
500
+ * `<Tldraw>` relies on this too, rather than passing a prop: the translation provider it renders
501
+ * above the editor has its locale fixed at mount, so an adapter read from there kept announcing
502
+ * shapes in the first language after the user switched.
503
+ *
504
+ * @internal
505
+ */
506
+ export const EditorI18nFallbackContext = React.createContext<{
507
+ current: TLI18nAdapter | null
508
+ } | null>(null)
509
+
487
510
  function TldrawEditorWithReadyStore({
488
511
  onMount,
489
512
  children,
@@ -505,6 +528,7 @@ function TldrawEditorWithReadyStore({
505
528
  assetUrls,
506
529
  themes,
507
530
  initialTheme,
531
+ i18n,
508
532
  }: Required<
509
533
  TldrawEditorProps & {
510
534
  store: TLStore
@@ -549,6 +573,18 @@ function TldrawEditorWithReadyStore({
549
573
  }
550
574
  }, [autoFocus, initialState, colorScheme, cameraOptions, deepLinks, themes, initialTheme])
551
575
 
576
+ // The adapter is called on each use rather than held as a value, so the editor can keep one
577
+ // stable function that reads whichever adapter is current. Listing `i18n` among the deps that
578
+ // recreate the editor instead tore it down — camera, selection, undo history — whenever the
579
+ // adapter's identity changed, which is every render for one written inline.
580
+ const i18nRef = useRef(i18n)
581
+ i18nRef.current = i18n
582
+ const fallbackI18nRef = useRef<TLI18nAdapter | null>(null)
583
+ const stableI18n = useCallback<TLI18nAdapter>(
584
+ () => (i18nRef.current ?? fallbackI18nRef.current ?? defaultI18n)(),
585
+ []
586
+ )
587
+
552
588
  useLayoutEffect(
553
589
  () => {
554
590
  const {
@@ -580,6 +616,7 @@ function TldrawEditorWithReadyStore({
580
616
  fontAssetUrls: assetUrls?.fonts,
581
617
  themes: themes,
582
618
  initialTheme: initialTheme,
619
+ i18n: stableI18n,
583
620
  })
584
621
  editor.licenseManager = licenseManager
585
622
 
@@ -620,6 +657,7 @@ function TldrawEditorWithReadyStore({
620
657
  licenseManager,
621
658
  getShapeVisibility,
622
659
  assetUrls,
660
+ stableI18n,
623
661
  ]
624
662
  )
625
663
 
@@ -767,10 +805,12 @@ function TldrawEditorWithReadyStore({
767
805
  <Crash crashingError={crashingError} />
768
806
  ) : (
769
807
  <EditorProvider editor={editor}>
770
- <Layout onMount={onMount}>
771
- {children ?? (Canvas ? <Canvas key={editor.contextId} /> : null)}
772
- <Watermark />
773
- </Layout>
808
+ <EditorI18nFallbackContext.Provider value={fallbackI18nRef}>
809
+ <Layout onMount={onMount}>
810
+ {children ?? (Canvas ? <Canvas key={editor.contextId} /> : null)}
811
+ <Watermark />
812
+ </Layout>
813
+ </EditorI18nFallbackContext.Provider>
774
814
  </EditorProvider>
775
815
  )}
776
816
  </OptionalErrorBoundary>
@@ -1,7 +1,13 @@
1
1
  import { useValue } from '@tldraw/state-react'
2
2
  import { noop } from '@tldraw/utils'
3
3
  import classNames from 'classnames'
4
- import { ComponentType, useEffect, useLayoutEffect, useRef, useState } from 'react'
4
+ import { ComponentType, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
5
+ import {
6
+ defineMessages,
7
+ translateMessage,
8
+ type TLI18nMessage,
9
+ type TLI18nValues,
10
+ } from '../../config/TLI18n'
5
11
  import { Editor } from '../../editor/Editor'
6
12
  import { getOwnerDocument } from '../../exports/domUtils'
7
13
  import { useEditorComponents } from '../../hooks/EditorComponentsContext'
@@ -12,6 +18,50 @@ import { ErrorBoundary } from '../ErrorBoundary'
12
18
 
13
19
  const BASE_ERROR_URL = 'https://github.com/tldraw/tldraw/issues/new'
14
20
 
21
+ const messages = defineMessages({
22
+ confirmResetTitle: { id: 'error.confirm-reset.title', defaultMessage: 'Are you sure?' },
23
+ confirmResetDescription: {
24
+ id: 'error.confirm-reset.description',
25
+ defaultMessage: 'Resetting your data will delete your drawing and cannot be undone.',
26
+ },
27
+ cancel: { id: 'error.cancel', defaultMessage: 'Cancel' },
28
+ resetData: { id: 'error.reset-data', defaultMessage: 'Reset data' },
29
+ title: { id: 'error.title', defaultMessage: 'Something went wrong' },
30
+ refreshHint: { id: 'error.refresh-hint', defaultMessage: 'Please refresh your browser.' },
31
+ resetHint: {
32
+ id: 'error.reset-hint',
33
+ defaultMessage:
34
+ 'If the issue continues after refreshing, you may need to reset the tldraw data stored on your device.',
35
+ },
36
+ noteLabel: { id: 'error.note-label', defaultMessage: 'Note:' },
37
+ resetNote: {
38
+ id: 'error.reset-note',
39
+ defaultMessage: 'Resetting will erase your current project and any unsaved work.',
40
+ },
41
+ messageLabel: { id: 'error.message-label', defaultMessage: 'Message:' },
42
+ stackTraceLabel: { id: 'error.stack-trace-label', defaultMessage: 'Stack trace:' },
43
+ copy: { id: 'error.copy', defaultMessage: 'Copy' },
44
+ copied: { id: 'error.copied', defaultMessage: 'Copied!' },
45
+ showDetails: { id: 'error.show-details', defaultMessage: 'Show details' },
46
+ hideDetails: { id: 'error.hide-details', defaultMessage: 'Hide details' },
47
+ refreshPage: { id: 'error.refresh-page', defaultMessage: 'Refresh page' },
48
+ })
49
+
50
+ // This renders because something already threw, so reading the editor is done defensively the way
51
+ // the rest of this component reads it: English beats a second crash on the error screen.
52
+ function useErrorMsg(editor: Editor | undefined) {
53
+ return useCallback(
54
+ (message: TLI18nMessage, values?: TLI18nValues) => {
55
+ try {
56
+ return translateMessage(editor?.i18n(), message, values)
57
+ } catch {
58
+ return message.defaultMessage
59
+ }
60
+ },
61
+ [editor]
62
+ )
63
+ }
64
+
15
65
  /** @public */
16
66
  export interface TLErrorFallbackProps {
17
67
  /** The error that was caught. */
@@ -28,6 +78,7 @@ export type TLErrorFallbackComponent = ComponentType<TLErrorFallbackProps>
28
78
 
29
79
  /** @public @react */
30
80
  export function DefaultErrorFallback({ error, editor }: TLErrorFallbackProps) {
81
+ const msg = useErrorMsg(editor)
31
82
  const containerRef = useRef<HTMLDivElement>(null)
32
83
  const [shouldShowError, setShouldShowError] = useState(process.env.NODE_ENV === 'development')
33
84
  const [didCopy, setDidCopy] = useState(false)
@@ -168,27 +219,24 @@ My browser: ${navigator.userAgent}`
168
219
  >
169
220
  {shouldShowResetConfirmation ? (
170
221
  <>
171
- <h2>Are you sure?</h2>
172
- <p>Resetting your data will delete your drawing and cannot be undone.</p>
222
+ <h2>{msg(messages.confirmResetTitle)}</h2>
223
+ <p>{msg(messages.confirmResetDescription)}</p>
173
224
  <div className="tl-error-boundary__content__actions">
174
225
  <button className="tlui-button" onClick={() => setShouldShowResetConfirmation(false)}>
175
- Cancel
226
+ {msg(messages.cancel)}
176
227
  </button>
177
228
  <button className="tlui-button tl-error-boundary__reset" onClick={resetLocalState}>
178
- Reset data
229
+ {msg(messages.resetData)}
179
230
  </button>
180
231
  </div>
181
232
  </>
182
233
  ) : (
183
234
  <>
184
- <h2>Something went wrong</h2>
185
- <p>Please refresh your browser.</p>
186
- <p>
187
- If the issue continues after refreshing, you may need to reset the tldraw data stored
188
- on your device.
189
- </p>
235
+ <h2>{msg(messages.title)}</h2>
236
+ <p>{msg(messages.refreshHint)}</p>
237
+ <p>{msg(messages.resetHint)}</p>
190
238
  <p>
191
- <strong>Note:</strong> Resetting will erase your current project and any unsaved work.
239
+ <strong>{msg(messages.noteLabel)}</strong> {msg(messages.resetNote)}
192
240
  </p>
193
241
  {process.env.NODE_ENV !== 'production' && (
194
242
  <p>
@@ -201,34 +249,34 @@ My browser: ${navigator.userAgent}`
201
249
  )}
202
250
  {shouldShowError && (
203
251
  <>
204
- Message:
252
+ {msg(messages.messageLabel)}
205
253
  <h4>
206
254
  <code>{errorMessage}</code>
207
255
  </h4>
208
- Stack trace:
256
+ {msg(messages.stackTraceLabel)}
209
257
  <div className="tl-error-boundary__content__error">
210
258
  <pre>
211
259
  <code>{errorStack ?? errorMessage}</code>
212
260
  </pre>
213
261
  <button className="tlui-button" onClick={copyError}>
214
- {didCopy ? 'Copied!' : 'Copy'}
262
+ {didCopy ? msg(messages.copied) : msg(messages.copy)}
215
263
  </button>
216
264
  </div>
217
265
  </>
218
266
  )}
219
267
  <div className="tl-error-boundary__content__actions">
220
268
  <button className="tlui-button" onClick={() => setShouldShowError(!shouldShowError)}>
221
- {shouldShowError ? 'Hide details' : 'Show details'}
269
+ {shouldShowError ? msg(messages.hideDetails) : msg(messages.showDetails)}
222
270
  </button>
223
271
  <div className="tl-error-boundary__content__actions__group">
224
272
  <button
225
273
  className="tlui-button tl-error-boundary__reset"
226
274
  onClick={() => setShouldShowResetConfirmation(true)}
227
275
  >
228
- Reset data
276
+ {msg(messages.resetData)}
229
277
  </button>
230
278
  <button className="tlui-button tl-error-boundary__refresh" onClick={refresh}>
231
- Refresh page
279
+ {msg(messages.refreshPage)}
232
280
  </button>
233
281
  </div>
234
282
  </div>
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ TLI18n,
4
+ defaultI18n,
5
+ defineMessages,
6
+ getDeclaredMessages,
7
+ translateMessage,
8
+ } from './TLI18n'
9
+
10
+ const messages = defineMessages({
11
+ somethingWentWrong: { id: 'error.title', defaultMessage: 'Something went wrong' },
12
+ shapesSelected: {
13
+ id: 'a11y.multiple-shapes',
14
+ defaultMessage: '{num, plural, one {# shape selected} other {# shapes selected}}',
15
+ },
16
+ // A plain placeholder, which the regex stub below can substitute. The plural above needs a
17
+ // real ICU formatter, so the adapter's own handling of it is covered in packages/tldraw.
18
+ addedFiles: { id: 'test.added-files', defaultMessage: 'Added {total} files' },
19
+ })
20
+
21
+ function i18nWith(catalog: Record<string, string>): TLI18n {
22
+ return {
23
+ locale: 'en',
24
+ dir: 'ltr',
25
+ // Mirrors the real adapter: a message the catalog doesn't have falls back to its own
26
+ // English, and either way the values are substituted.
27
+ translate: (message, values) => {
28
+ const text = catalog[message.id] ?? message.defaultMessage
29
+ return text.replace(/\{(\w+)[^}]*\}/g, (_, name) => String(values?.[name] ?? ''))
30
+ },
31
+ }
32
+ }
33
+
34
+ describe('translateMessage', () => {
35
+ it('uses the translation when the catalog has one', () => {
36
+ const i18n = i18nWith({ 'error.title': 'Etwas ist schiefgelaufen' })
37
+ expect(translateMessage(i18n, messages.somethingWentWrong)).toBe('Etwas ist schiefgelaufen')
38
+ })
39
+
40
+ // @tldraw/editor is usable with none of tldraw's UI, so there may be no i18n at all.
41
+ it('falls back to the English when there is no i18n', () => {
42
+ expect(translateMessage(null, messages.somethingWentWrong)).toBe('Something went wrong')
43
+ expect(translateMessage(undefined, messages.somethingWentWrong)).toBe('Something went wrong')
44
+ })
45
+
46
+ // A message that hasn't been through Lokalise yet is absent from every catalog. Showing the id
47
+ // to someone would be worse than English.
48
+ it('falls back to the English when the catalog has no entry', () => {
49
+ expect(translateMessage(i18nWith({}), messages.somethingWentWrong)).toBe('Something went wrong')
50
+ expect(translateMessage(defaultI18n(), messages.somethingWentWrong)).toBe(
51
+ 'Something went wrong'
52
+ )
53
+ })
54
+
55
+ // The adapter gets the whole message, so it can substitute into the English too. Falling back
56
+ // to the raw source instead put a plural message's own ICU into an upload toast.
57
+ it('substitutes values into the English it falls back to', () => {
58
+ expect(translateMessage(i18nWith({}), messages.addedFiles, { total: 3 })).toBe('Added 3 files')
59
+ })
60
+
61
+ it('passes values through to the translation', () => {
62
+ const i18n = i18nWith({ 'a11y.multiple-shapes': '{num} Formen ausgewählt' })
63
+ expect(translateMessage(i18n, messages.shapesSelected, { num: 3 })).toBe('3 Formen ausgewählt')
64
+ })
65
+ })
66
+
67
+ describe('defineMessages', () => {
68
+ it('returns the messages unchanged', () => {
69
+ const declared = { a: { id: 'x', defaultMessage: 'X' } }
70
+ expect(defineMessages(declared)).toEqual(declared)
71
+ })
72
+
73
+ // The SDK bundles no separate English catalog; a key looked up by id resolves against this.
74
+ it('records the English of each message it declares', () => {
75
+ defineMessages({
76
+ a: { id: 'test.declared', defaultMessage: 'Declared' },
77
+ b: { defaultMessage: 'No id' },
78
+ })
79
+ expect(getDeclaredMessages()).toMatchObject({
80
+ 'test.declared': 'Declared',
81
+ 'No id': 'No id',
82
+ })
83
+ })
84
+ })