@inkandswitch/patchwork 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @inkandswitch/patchwork
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 98be594: Collapse `siteName`, `title`, and `setup({name})` into a single `title`.
8
+
9
+ A site's name was three options across two files: `siteName` and `title` in the vite config, `name` at `setup`. They fed the same handful of strings and could disagree with each other.
10
+
11
+ `title` is now the only one. It names the html `<title>`, `apple-mobile-web-app-title`, and the manifest's `name`/`short_name` as before, and is also emitted as the `__SITE_TITLE__` define, which supplies the brand word the router appends to the document title as `"<doc> | <title>"`. It defaults to `"Patchwork"`.
12
+
13
+ - `siteName` and the `__SITE_NAME__` define are removed. If you used `siteName` only for display, rename it to `title`; if you relied on it to namespace storage, see `storagePrefix`.
14
+ - `setup({name})` is now `setup({title})`, and is only needed to override the build-time value.
15
+
16
+ - 98be594: Namespace IndexedDB and peer ids with a new build-time `storagePrefix` option.
17
+
18
+ The tab and the shared automerge worker are separate bundles that must open the same databases. Both now read the name from one place, `@inkandswitch/patchwork-bootloader/storage`, resolved from the `__STORAGE_PREFIX__` define the vite plugin emits unconditionally.
19
+
20
+ Previously each side resolved `__SITE_NAME__` itself with a different fallback — `"patchwork.inkandswitch.com"` in the worker, `"patchwork"` in the tab — so a site that never set `siteName` had its tab and worker on two different keyhive databases, and one that passed `setup({name})` split them the same way, since a runtime option never reaches the worker.
21
+
22
+ - `storagePrefix` defaults to `"patchwork"` and is settable only in the build config. Sites sharing an origin must use distinct prefixes. It is deliberately not derived from any display name: changing it points a site at empty storage, so a rebrand must not be able to change it by accident.
23
+ - Sites that relied on `siteName` to namespace their storage must now set `storagePrefix` explicitly to that same value to keep their existing databases.
24
+ - `createRepo` in `@inkandswitch/patchwork` no longer takes a site name argument.
25
+
26
+ ### Patch Changes
27
+
28
+ - Updated dependencies [98be594]
29
+ - @inkandswitch/patchwork-bootloader@0.6.0
30
+
3
31
  ## 0.4.0
4
32
 
5
33
  ### Minor Changes
package/dist/client.d.ts CHANGED
@@ -12,7 +12,8 @@ import "@inkandswitch/patchwork-elements";
12
12
  import "@inkandswitch/patchwork-providers";
13
13
 
14
14
  declare global {
15
- const __SITE_NAME__: string;
15
+ const __SITE_TITLE__: string;
16
+ const __STORAGE_PREFIX__: string;
16
17
 
17
18
  interface ImportMetaEnv {
18
19
  /**
package/dist/index.js CHANGED
@@ -53,8 +53,8 @@ export function setup(options = {}) {
53
53
  }
54
54
  export default setup;
55
55
  async function doSetup(options) {
56
- const siteName = options.name ??
57
- (typeof __SITE_NAME__ !== "undefined" ? __SITE_NAME__ : "patchwork");
56
+ const siteTitle = options.title ??
57
+ (typeof __SITE_TITLE__ !== "undefined" ? __SITE_TITLE__ : "Patchwork");
58
58
  const moduleSources = resolveDefaultModules(options);
59
59
  const routing = options.routing ?? "hash";
60
60
  log("booting", options);
@@ -85,7 +85,7 @@ async function doSetup(options) {
85
85
  }
86
86
  });
87
87
  let workerAdapter = new MessageChannelNetworkAdapter(workerPort);
88
- ({ repo, hive, signerIdentity } = await createRepo(siteName, workerAdapter));
88
+ ({ repo, hive, signerIdentity } = await createRepo(workerAdapter));
89
89
  // The worker was recreated with cold state: wire the repo onto the fresh
90
90
  // port and drop the adapter stranded on the dead one.
91
91
  const bootHive = hive;
@@ -154,7 +154,7 @@ async function doSetup(options) {
154
154
  rootElement,
155
155
  repo,
156
156
  accountDocHandle,
157
- siteName,
157
+ siteTitle,
158
158
  });
159
159
  }
160
160
  installReveal(rootElement, router, toolsLoaded);
package/dist/repo.d.ts CHANGED
@@ -3,7 +3,7 @@ import { type AutomergeRepoKeyhive } from "@automerge/automerge-repo-keyhive";
3
3
  import setupServiceWorker from "@inkandswitch/patchwork-bootloader";
4
4
  import type { SignerIdentity } from "./types.js";
5
5
  export declare function initWasm(): Promise<void>;
6
- export declare function createRepo(siteName: string, workerAdapter: MessageChannelNetworkAdapter): Promise<{
6
+ export declare function createRepo(workerAdapter: MessageChannelNetworkAdapter): Promise<{
7
7
  repo: Repo;
8
8
  hive?: AutomergeRepoKeyhive;
9
9
  signerIdentity?: SignerIdentity;
package/dist/repo.js CHANGED
@@ -5,6 +5,7 @@ import { initKeyhiveWasm, initializeAutomergeRepoKeyhiveWithRepo, } from "@autom
5
5
  // @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
6
6
  import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
7
7
  import { MemorySigner } from "@automerge/automerge-subduction/slim";
8
+ import { keyhiveStorageName, storagePrefix, } from "@inkandswitch/patchwork-bootloader/storage";
8
9
  import debug from "debug";
9
10
  const log = debug("patchwork:setup:repo");
10
11
  const syncServer = typeof __SYNC_SERVER__ !== "undefined"
@@ -27,14 +28,14 @@ export function initWasm() {
27
28
  }
28
29
  return wasmReady;
29
30
  }
30
- export async function createRepo(siteName, workerAdapter) {
31
+ export async function createRepo(workerAdapter) {
31
32
  if (syncServer.keyhive) {
32
33
  log("setting up keyhive");
33
34
  initKeyhiveWasm();
34
35
  const { hive, repo } = await initializeAutomergeRepoKeyhiveWithRepo({
35
36
  createRepo: (repoConfig) => new Repo(repoConfig),
36
- storage: new IndexedDBWorkerStorageAdapter(`${siteName}-keyhive`),
37
- peerIdSuffix: siteName + Math.random().toString(36).slice(2),
37
+ storage: new IndexedDBWorkerStorageAdapter(keyhiveStorageName),
38
+ peerIdSuffix: storagePrefix + Math.random().toString(36).slice(2),
38
39
  networkAdapter: workerAdapter,
39
40
  automaticArchiveIngestion: true,
40
41
  cachingMode: "periodic",
@@ -61,7 +62,7 @@ export async function createRepo(siteName, workerAdapter) {
61
62
  return peerId.includes("automerge-worker");
62
63
  },
63
64
  enableRemoteHeadsGossiping: true,
64
- peerId: `${siteName}-tab-${crypto.randomUUID()}`,
65
+ peerId: `${storagePrefix}-tab-${crypto.randomUUID()}`,
65
66
  });
66
67
  const signerIdentity = {
67
68
  peerId: signer.peerId().toString(),
package/dist/router.d.ts CHANGED
@@ -9,10 +9,10 @@ export interface RouterParams {
9
9
  rootElement: HTMLElement;
10
10
  repo: Repo;
11
11
  accountDocHandle: DocHandle<AccountDoc>;
12
- siteName: string;
12
+ siteTitle: string;
13
13
  }
14
14
  export interface Router {
15
15
  /** Apply `location.hash` to the view. */
16
16
  route(): Promise<void>;
17
17
  }
18
- export declare function createRouter({ rootElement, repo, accountDocHandle, siteName, }: RouterParams): Router;
18
+ export declare function createRouter({ rootElement, repo, accountDocHandle, siteTitle, }: RouterParams): Router;
package/dist/router.js CHANGED
@@ -1,4 +1,4 @@
1
- import { isValidAutomergeUrl, isValidDocumentId, stringifyAutomergeUrl, } from "@automerge/vanillajs/slim";
1
+ import { isValidAutomergeUrl, isValidDocumentId, parseAutomergeUrl, stringifyAutomergeUrl, } from "@automerge/vanillajs/slim";
2
2
  import { openDocument } from "@inkandswitch/patchwork-elements";
3
3
  import { getRegistry, } from "@inkandswitch/patchwork-plugins";
4
4
  // Legacy big-patchwork hash shape: `<slug>--<documentId>[?…]`. The slug can
@@ -6,12 +6,12 @@ import { getRegistry, } from "@inkandswitch/patchwork-plugins";
6
6
  // anchor on the `--` before the base58 document id rather than a strict slug
7
7
  // charset.
8
8
  const BIG_PATCHWORK_HASH_REGEX = /^(?<title>[^=&?/#]*)--(?<docId>[1-9A-HJ-NP-Za-km-z]+)/;
9
- // The `doc=` value is an automerge URL, kept literal rather than
9
+ // The `doc=` and `draft=` values are automerge URLs, kept literal rather than
10
10
  // percent-encoded so links stay readable.
11
- const RAW_HASH_KEYS = new Set(["doc"]);
11
+ const RAW_HASH_KEYS = new Set(["doc", "draft"]);
12
12
  // A stable order means re-serializing the same logical params is
13
13
  // byte-identical, avoiding spurious `hashchange` round-trips.
14
- const HASH_KEY_ORDER = ["doc", "tool", "type", "title", "frame"];
14
+ const HASH_KEY_ORDER = ["doc", "tool", "type", "title", "frame", "draft"];
15
15
  function serializeHashParams(params) {
16
16
  const keys = [...HASH_KEY_ORDER, ...params.keys()];
17
17
  const parts = [];
@@ -47,7 +47,7 @@ function registeredFrameToolId() {
47
47
  .filter((tool) => !!tool.tags?.includes("frame-tool") && !tool.unlisted)
48
48
  .at(0)?.id;
49
49
  }
50
- export function createRouter({ rootElement, repo, accountDocHandle, siteName, }) {
50
+ export function createRouter({ rootElement, repo, accountDocHandle, siteTitle, }) {
51
51
  const route = async () => {
52
52
  // The first call seeds the root view's tool/doc so it can mount; later
53
53
  // calls reconcile the mounted view with the hash.
@@ -107,6 +107,14 @@ export function createRouter({ rootElement, repo, accountDocHandle, siteName, })
107
107
  // `doc` is the full automerge URL, so heads live inside it and the separate
108
108
  // `heads=` param is gone.
109
109
  params.delete("heads");
110
+ // `draft` is doc-scoped (owned by the drafts plugin, opaque here):
111
+ // navigating to a different document invalidates the selection, while
112
+ // same-doc navigation (tool switches, heads changes) keeps it.
113
+ const prevDoc = docParamToUrl(params.get("doc"));
114
+ if (prevDoc &&
115
+ parseAutomergeUrl(prevDoc).documentId !== parseAutomergeUrl(url).documentId) {
116
+ params.delete("draft");
117
+ }
110
118
  params.set("doc", url);
111
119
  for (const [key, value] of [
112
120
  ["tool", toolId],
@@ -130,7 +138,7 @@ export function createRouter({ rootElement, repo, accountDocHandle, siteName, })
130
138
  return;
131
139
  const docTitle = datatype.module.getTitle(doc);
132
140
  if (docTitle)
133
- document.title = `${docTitle} | ${siteName}`;
141
+ document.title = `${docTitle} | ${siteTitle}`;
134
142
  }
135
143
  catch (e) {
136
144
  console.error("Failed to update document title", e);
@@ -1,4 +1,4 @@
1
- import type { PatchworkSiteOptions } from "./options.js";
1
+ import { type PatchworkSiteOptions } from "./options.js";
2
2
  export declare function escapeHtml(value: string): string;
3
3
  /** Builds the generated index.html as a plain string — no bundler involved. */
4
4
  export declare function buildHtml(options: PatchworkSiteOptions): string;
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_TITLE } from "./options.js";
1
2
  import { resolveSyncServers, PRELOAD_WASM_ASSETS } from "./sync-servers.js";
2
3
  import { ICON_SPECS } from "./icons.js";
3
4
  const HTML_ESCAPES = {
@@ -12,7 +13,7 @@ export function escapeHtml(value) {
12
13
  }
13
14
  /** Builds the generated index.html as a plain string — no bundler involved. */
14
15
  export function buildHtml(options) {
15
- const title = options.title ?? options.siteName ?? "Patchwork";
16
+ const title = options.title ?? DEFAULT_TITLE;
16
17
  const lang = (options.html && options.html.lang) || "en";
17
18
  const entry = options.entry ?? "/src/main.ts";
18
19
  const syncServers = resolveSyncServers(options);
@@ -1,3 +1,3 @@
1
- import type { PatchworkSiteOptions } from "./options.js";
1
+ import { type PatchworkSiteOptions } from "./options.js";
2
2
  /** Builds the generated manifest.webmanifest object — no bundler involved. */
3
3
  export declare function buildManifest(options: PatchworkSiteOptions): Record<string, unknown>;
@@ -1,7 +1,8 @@
1
+ import { DEFAULT_TITLE } from "./options.js";
1
2
  import { ICON_SPECS } from "./icons.js";
2
3
  /** Builds the generated manifest.webmanifest object — no bundler involved. */
3
4
  export function buildManifest(options) {
4
- const title = options.title ?? options.siteName ?? "Patchwork";
5
+ const title = options.title ?? DEFAULT_TITLE;
5
6
  const icons = !options.icons
6
7
  ? []
7
8
  : ICON_SPECS.filter((spec) => spec.fileName === "apple-touch-icon.png" || spec.manifestPurpose).map((spec) => ({
@@ -37,10 +37,23 @@ export type PatchworkSyncServersOptions = {
37
37
  /** wss:// URL for the legacy automerge-repo sync-server channel (connected on demand via connectClassicSync). Default: wss://sync3.automerge.org. Pass false to skip its preconnect hint. */
38
38
  classic?: string | false;
39
39
  } & PatchworkPrimarySyncServerOptions;
40
+ export declare const DEFAULT_TITLE = "Patchwork";
40
41
  export interface PatchworkSiteOptions {
41
- /** -> __SITE_NAME__ define */
42
- siteName?: string;
43
- /** <title>, apple-mobile-web-app-title, manifest name */
42
+ /**
43
+ * Namespace for this site's IndexedDB databases and peer ids
44
+ * (-> __STORAGE_PREFIX__ define). Defaults to `"patchwork"`. Sites sharing
45
+ * an origin MUST use distinct prefixes.
46
+ *
47
+ * The tab and the shared automerge worker are separate bundles that have to
48
+ * open the same databases, so this is settable only here, where both of them
49
+ * receive it. Changing it on an existing site points it at empty storage.
50
+ */
51
+ storagePrefix?: string;
52
+ /**
53
+ * This site's name: `<title>`, apple-mobile-web-app-title, manifest name,
54
+ * and — via the __SITE_TITLE__ define — the brand word the router appends to
55
+ * the document title as `"<doc> | <title>"`. Defaults to `"Patchwork"`.
56
+ */
44
57
  title?: string;
45
58
  /** manifest short_name (defaults to title) */
46
59
  shortName?: string;
@@ -1 +1 @@
1
- export {};
1
+ export const DEFAULT_TITLE = "Patchwork";
package/dist/types.d.ts CHANGED
@@ -47,13 +47,14 @@ export interface PatchworkOptions {
47
47
  accountKey?: string;
48
48
  createAccount?: AccountCreator;
49
49
  /**
50
- * Brand word for this site: appended to the document title as
51
- * `"<doc> | <name>"` when a document is open (the separator is provided
52
- * for you), and used to namespace this site's storage and peer ids.
50
+ * This site's name, appended to the document title as `"<doc> | <title>"`
51
+ * when a document is open (the separator is provided for you).
53
52
  *
54
- * Defaults to the build-time `__SITE_NAME__` define, then `"patchwork"`.
53
+ * Defaults to the build-time `__SITE_TITLE__` define the vite plugin's
54
+ * `title` option, which also names the html `<title>` and the manifest —
55
+ * then `"Patchwork"`.
55
56
  */
56
- name?: string;
57
+ title?: string;
57
58
  /** DOM id of the `<patchwork-view>` hosting the root tool. Defaults to "root". */
58
59
  rootElementId?: string;
59
60
  /**
@@ -2,7 +2,8 @@ import type { Plugin } from "vite";
2
2
  import wasm from "vite-plugin-wasm";
3
3
  import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
4
4
  /**
5
- * Owns envPrefix, define (__SITE_NAME__/sync-server configuration),
5
+ * Owns envPrefix, define (__SITE_TITLE__/__STORAGE_PREFIX__/sync-server
6
+ * configuration),
6
7
  * server/preview CORS defaults, worker format + the wasm plugin, and build
7
8
  * defaults (firefox150 target, unminified, sourcemapped) — everything a site
8
9
  * used to hand-write in its own vite.config.ts. Each is switched off
@@ -1,8 +1,11 @@
1
1
  import wasm from "vite-plugin-wasm";
2
+ import { DEFAULT_STORAGE_PREFIX } from "@inkandswitch/patchwork-bootloader/storage";
3
+ import { DEFAULT_TITLE } from "../site-kit/options.js";
2
4
  import { DEFAULT_SYNC_SERVERS, resolvePrimarySyncServer, } from "../site-kit/sync-servers.js";
3
5
  const CORS_HEADERS = { "Access-Control-Allow-Origin": "*" };
4
6
  /**
5
- * Owns envPrefix, define (__SITE_NAME__/sync-server configuration),
7
+ * Owns envPrefix, define (__SITE_TITLE__/__STORAGE_PREFIX__/sync-server
8
+ * configuration),
6
9
  * server/preview CORS defaults, worker format + the wasm plugin, and build
7
10
  * defaults (firefox150 target, unminified, sourcemapped) — everything a site
8
11
  * used to hand-write in its own vite.config.ts. Each is switched off
@@ -16,13 +19,14 @@ export function configPlugin(options = {}) {
16
19
  const classicSyncServer = options.syncServers && typeof options.syncServers.classic === "string"
17
20
  ? options.syncServers.classic
18
21
  : DEFAULT_SYNC_SERVERS.classic;
22
+ // __STORAGE_PREFIX__ is defined unconditionally: the tab and the shared
23
+ // automerge worker resolve it separately, and only a define reaches both.
19
24
  const define = {
20
25
  __SYNC_SERVER__: JSON.stringify(primarySyncServer),
21
26
  __CLASSIC_SYNC_SERVER__: JSON.stringify(classicSyncServer),
27
+ __SITE_TITLE__: JSON.stringify(options.title ?? DEFAULT_TITLE),
28
+ __STORAGE_PREFIX__: JSON.stringify(options.storagePrefix ?? DEFAULT_STORAGE_PREFIX),
22
29
  };
23
- if (options.siteName) {
24
- define.__SITE_NAME__ = JSON.stringify(options.siteName);
25
- }
26
30
  return {
27
31
  envPrefix: ["VITE_", "PATCHWORK_"],
28
32
  define,
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "git+https://github.com/inkandswitch/patchwork-system.git",
6
6
  "directory": "core/patchwork"
7
7
  },
8
- "version": "0.4.0",
8
+ "version": "0.5.0",
9
9
  "author": "Ink & Switch",
10
10
  "type": "module",
11
11
  "license": "MIT",
@@ -42,11 +42,11 @@
42
42
  "debug": "^4.4.3",
43
43
  "sharp": "^0.35.3",
44
44
  "vite-plugin-wasm": "^3.6.0",
45
- "@inkandswitch/patchwork-plugins": "^1.2.0",
46
- "@inkandswitch/patchwork-providers": "^0.5.0",
47
45
  "@inkandswitch/patchwork-elements": "^6.0.0",
46
+ "@inkandswitch/patchwork-plugins": "^1.2.0",
47
+ "@inkandswitch/patchwork-bootloader": "^0.6.0",
48
48
  "@inkandswitch/patchwork-filesystem": "^0.2.5",
49
- "@inkandswitch/patchwork-bootloader": "^0.5.4"
49
+ "@inkandswitch/patchwork-providers": "^0.5.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "esbuild": "^0.23.1",
package/src/client.d.ts CHANGED
@@ -12,7 +12,8 @@ import "@inkandswitch/patchwork-elements";
12
12
  import "@inkandswitch/patchwork-providers";
13
13
 
14
14
  declare global {
15
- const __SITE_NAME__: string;
15
+ const __SITE_TITLE__: string;
16
+ const __STORAGE_PREFIX__: string;
16
17
 
17
18
  interface ImportMetaEnv {
18
19
  /**
package/src/index.ts CHANGED
@@ -64,7 +64,7 @@ import { createDefaultAccount } from "./createAccount.js";
64
64
 
65
65
  const log = debug("patchwork:setup");
66
66
 
67
- declare const __SITE_NAME__: string;
67
+ declare const __SITE_TITLE__: string;
68
68
 
69
69
  declare global {
70
70
  interface Window {
@@ -114,9 +114,9 @@ export function setup(options: PatchworkOptions = {}): Promise<Patchwork> {
114
114
  export default setup;
115
115
 
116
116
  async function doSetup(options: PatchworkOptions): Promise<Patchwork> {
117
- const siteName =
118
- options.name ??
119
- (typeof __SITE_NAME__ !== "undefined" ? __SITE_NAME__ : "patchwork");
117
+ const siteTitle =
118
+ options.title ??
119
+ (typeof __SITE_TITLE__ !== "undefined" ? __SITE_TITLE__ : "Patchwork");
120
120
  const moduleSources = resolveDefaultModules(options);
121
121
  const routing = options.routing ?? "hash";
122
122
 
@@ -151,10 +151,7 @@ async function doSetup(options: PatchworkOptions): Promise<Patchwork> {
151
151
  });
152
152
 
153
153
  let workerAdapter = new MessageChannelNetworkAdapter(workerPort);
154
- ({ repo, hive, signerIdentity } = await createRepo(
155
- siteName,
156
- workerAdapter
157
- ));
154
+ ({ repo, hive, signerIdentity } = await createRepo(workerAdapter));
158
155
 
159
156
  // The worker was recreated with cold state: wire the repo onto the fresh
160
157
  // port and drop the adapter stranded on the dead one.
@@ -246,7 +243,7 @@ async function doSetup(options: PatchworkOptions): Promise<Patchwork> {
246
243
  rootElement,
247
244
  repo,
248
245
  accountDocHandle,
249
- siteName,
246
+ siteTitle,
250
247
  });
251
248
  }
252
249
 
package/src/repo.ts CHANGED
@@ -17,6 +17,10 @@ import {
17
17
  import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
18
18
  import { MemorySigner } from "@automerge/automerge-subduction/slim";
19
19
  import setupServiceWorker from "@inkandswitch/patchwork-bootloader";
20
+ import {
21
+ keyhiveStorageName,
22
+ storagePrefix,
23
+ } from "@inkandswitch/patchwork-bootloader/storage";
20
24
  import type { SignerIdentity } from "./types.js";
21
25
  import debug from "debug";
22
26
 
@@ -50,7 +54,6 @@ export function initWasm(): Promise<void> {
50
54
  }
51
55
 
52
56
  export async function createRepo(
53
- siteName: string,
54
57
  workerAdapter: MessageChannelNetworkAdapter
55
58
  ): Promise<{
56
59
  repo: Repo;
@@ -62,8 +65,8 @@ export async function createRepo(
62
65
  initKeyhiveWasm();
63
66
  const { hive, repo } = await initializeAutomergeRepoKeyhiveWithRepo({
64
67
  createRepo: (repoConfig) => new Repo(repoConfig),
65
- storage: new IndexedDBWorkerStorageAdapter(`${siteName}-keyhive`),
66
- peerIdSuffix: siteName + Math.random().toString(36).slice(2),
68
+ storage: new IndexedDBWorkerStorageAdapter(keyhiveStorageName),
69
+ peerIdSuffix: storagePrefix + Math.random().toString(36).slice(2),
67
70
  networkAdapter: workerAdapter,
68
71
  automaticArchiveIngestion: true,
69
72
  cachingMode: "periodic",
@@ -91,7 +94,8 @@ export async function createRepo(
91
94
  return peerId.includes("automerge-worker");
92
95
  },
93
96
  enableRemoteHeadsGossiping: true,
94
- peerId: `${siteName}-tab-${crypto.randomUUID()}` as AutomergeRepo.PeerId,
97
+ peerId:
98
+ `${storagePrefix}-tab-${crypto.randomUUID()}` as AutomergeRepo.PeerId,
95
99
  });
96
100
  const signerIdentity = {
97
101
  peerId: signer.peerId().toString(),
package/src/router.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  isValidAutomergeUrl,
3
3
  isValidDocumentId,
4
+ parseAutomergeUrl,
4
5
  stringifyAutomergeUrl,
5
6
  type AutomergeUrl,
6
7
  type DocHandle,
@@ -23,12 +24,12 @@ import {
23
24
  const BIG_PATCHWORK_HASH_REGEX =
24
25
  /^(?<title>[^=&?/#]*)--(?<docId>[1-9A-HJ-NP-Za-km-z]+)/;
25
26
 
26
- // The `doc=` value is an automerge URL, kept literal rather than
27
+ // The `doc=` and `draft=` values are automerge URLs, kept literal rather than
27
28
  // percent-encoded so links stay readable.
28
- const RAW_HASH_KEYS = new Set(["doc"]);
29
+ const RAW_HASH_KEYS = new Set(["doc", "draft"]);
29
30
  // A stable order means re-serializing the same logical params is
30
31
  // byte-identical, avoiding spurious `hashchange` round-trips.
31
- const HASH_KEY_ORDER = ["doc", "tool", "type", "title", "frame"];
32
+ const HASH_KEY_ORDER = ["doc", "tool", "type", "title", "frame", "draft"];
32
33
 
33
34
  function serializeHashParams(params: URLSearchParams): string {
34
35
  const keys = [...HASH_KEY_ORDER, ...params.keys()];
@@ -64,7 +65,7 @@ export interface RouterParams {
64
65
  rootElement: HTMLElement;
65
66
  repo: Repo;
66
67
  accountDocHandle: DocHandle<AccountDoc>;
67
- siteName: string;
68
+ siteTitle: string;
68
69
  }
69
70
 
70
71
  export interface Router {
@@ -82,7 +83,7 @@ export function createRouter({
82
83
  rootElement,
83
84
  repo,
84
85
  accountDocHandle,
85
- siteName,
86
+ siteTitle,
86
87
  }: RouterParams): Router {
87
88
  const route = async () => {
88
89
  // The first call seeds the root view's tool/doc so it can mount; later
@@ -166,6 +167,16 @@ export function createRouter({
166
167
  // `doc` is the full automerge URL, so heads live inside it and the separate
167
168
  // `heads=` param is gone.
168
169
  params.delete("heads");
170
+ // `draft` is doc-scoped (owned by the drafts plugin, opaque here):
171
+ // navigating to a different document invalidates the selection, while
172
+ // same-doc navigation (tool switches, heads changes) keeps it.
173
+ const prevDoc = docParamToUrl(params.get("doc"));
174
+ if (
175
+ prevDoc &&
176
+ parseAutomergeUrl(prevDoc).documentId !== parseAutomergeUrl(url).documentId
177
+ ) {
178
+ params.delete("draft");
179
+ }
169
180
  params.set("doc", url);
170
181
  for (const [key, value] of [
171
182
  ["tool", toolId],
@@ -192,7 +203,7 @@ export function createRouter({
192
203
  const docTitle = (datatype.module as DatatypeImplementation).getTitle(
193
204
  doc
194
205
  );
195
- if (docTitle) document.title = `${docTitle} | ${siteName}`;
206
+ if (docTitle) document.title = `${docTitle} | ${siteTitle}`;
196
207
  } catch (e) {
197
208
  console.error("Failed to update document title", e);
198
209
  }
@@ -1,4 +1,4 @@
1
- import type { PatchworkSiteOptions } from "./options.js";
1
+ import { DEFAULT_TITLE, type PatchworkSiteOptions } from "./options.js";
2
2
  import { resolveSyncServers, PRELOAD_WASM_ASSETS } from "./sync-servers.js";
3
3
  import { ICON_SPECS } from "./icons.js";
4
4
 
@@ -16,7 +16,7 @@ export function escapeHtml(value: string): string {
16
16
 
17
17
  /** Builds the generated index.html as a plain string — no bundler involved. */
18
18
  export function buildHtml(options: PatchworkSiteOptions): string {
19
- const title = options.title ?? options.siteName ?? "Patchwork";
19
+ const title = options.title ?? DEFAULT_TITLE;
20
20
  const lang = (options.html && options.html.lang) || "en";
21
21
  const entry = options.entry ?? "/src/main.ts";
22
22
  const syncServers = resolveSyncServers(options);
@@ -1,11 +1,11 @@
1
- import type { PatchworkSiteOptions } from "./options.js";
1
+ import { DEFAULT_TITLE, type PatchworkSiteOptions } from "./options.js";
2
2
  import { ICON_SPECS } from "./icons.js";
3
3
 
4
4
  /** Builds the generated manifest.webmanifest object — no bundler involved. */
5
5
  export function buildManifest(
6
6
  options: PatchworkSiteOptions
7
7
  ): Record<string, unknown> {
8
- const title = options.title ?? options.siteName ?? "Patchwork";
8
+ const title = options.title ?? DEFAULT_TITLE;
9
9
  const icons = !options.icons
10
10
  ? []
11
11
  : ICON_SPECS.filter(
@@ -42,10 +42,24 @@ export type PatchworkSyncServersOptions = {
42
42
  classic?: string | false;
43
43
  } & PatchworkPrimarySyncServerOptions;
44
44
 
45
+ export const DEFAULT_TITLE = "Patchwork";
46
+
45
47
  export interface PatchworkSiteOptions {
46
- /** -> __SITE_NAME__ define */
47
- siteName?: string;
48
- /** <title>, apple-mobile-web-app-title, manifest name */
48
+ /**
49
+ * Namespace for this site's IndexedDB databases and peer ids
50
+ * (-> __STORAGE_PREFIX__ define). Defaults to `"patchwork"`. Sites sharing
51
+ * an origin MUST use distinct prefixes.
52
+ *
53
+ * The tab and the shared automerge worker are separate bundles that have to
54
+ * open the same databases, so this is settable only here, where both of them
55
+ * receive it. Changing it on an existing site points it at empty storage.
56
+ */
57
+ storagePrefix?: string;
58
+ /**
59
+ * This site's name: `<title>`, apple-mobile-web-app-title, manifest name,
60
+ * and — via the __SITE_TITLE__ define — the brand word the router appends to
61
+ * the document title as `"<doc> | <title>"`. Defaults to `"Patchwork"`.
62
+ */
49
63
  title?: string;
50
64
  /** manifest short_name (defaults to title) */
51
65
  shortName?: string;
package/src/types.ts CHANGED
@@ -65,13 +65,14 @@ export interface PatchworkOptions {
65
65
  createAccount?: AccountCreator;
66
66
 
67
67
  /**
68
- * Brand word for this site: appended to the document title as
69
- * `"<doc> | <name>"` when a document is open (the separator is provided
70
- * for you), and used to namespace this site's storage and peer ids.
68
+ * This site's name, appended to the document title as `"<doc> | <title>"`
69
+ * when a document is open (the separator is provided for you).
71
70
  *
72
- * Defaults to the build-time `__SITE_NAME__` define, then `"patchwork"`.
71
+ * Defaults to the build-time `__SITE_TITLE__` define the vite plugin's
72
+ * `title` option, which also names the html `<title>` and the manifest —
73
+ * then `"Patchwork"`.
73
74
  */
74
- name?: string;
75
+ title?: string;
75
76
 
76
77
  /** DOM id of the `<patchwork-view>` hosting the root tool. Defaults to "root". */
77
78
  rootElementId?: string;
@@ -1,5 +1,7 @@
1
1
  import type { Plugin } from "vite";
2
2
  import wasm from "vite-plugin-wasm";
3
+ import { DEFAULT_STORAGE_PREFIX } from "@inkandswitch/patchwork-bootloader/storage";
4
+ import { DEFAULT_TITLE } from "../site-kit/options.js";
3
5
  import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
4
6
  import {
5
7
  DEFAULT_SYNC_SERVERS,
@@ -9,7 +11,8 @@ import {
9
11
  const CORS_HEADERS = { "Access-Control-Allow-Origin": "*" };
10
12
 
11
13
  /**
12
- * Owns envPrefix, define (__SITE_NAME__/sync-server configuration),
14
+ * Owns envPrefix, define (__SITE_TITLE__/__STORAGE_PREFIX__/sync-server
15
+ * configuration),
13
16
  * server/preview CORS defaults, worker format + the wasm plugin, and build
14
17
  * defaults (firefox150 target, unminified, sourcemapped) — everything a site
15
18
  * used to hand-write in its own vite.config.ts. Each is switched off
@@ -26,13 +29,16 @@ export function configPlugin(
26
29
  options.syncServers && typeof options.syncServers.classic === "string"
27
30
  ? options.syncServers.classic
28
31
  : DEFAULT_SYNC_SERVERS.classic;
32
+ // __STORAGE_PREFIX__ is defined unconditionally: the tab and the shared
33
+ // automerge worker resolve it separately, and only a define reaches both.
29
34
  const define: Record<string, string> = {
30
35
  __SYNC_SERVER__: JSON.stringify(primarySyncServer),
31
36
  __CLASSIC_SYNC_SERVER__: JSON.stringify(classicSyncServer),
37
+ __SITE_TITLE__: JSON.stringify(options.title ?? DEFAULT_TITLE),
38
+ __STORAGE_PREFIX__: JSON.stringify(
39
+ options.storagePrefix ?? DEFAULT_STORAGE_PREFIX
40
+ ),
32
41
  };
33
- if (options.siteName) {
34
- define.__SITE_NAME__ = JSON.stringify(options.siteName);
35
- }
36
42
 
37
43
  return {
38
44
  envPrefix: ["VITE_", "PATCHWORK_"],