nebula-cms 0.1.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/.claude/settings.local.json +42 -0
- package/.github/workflows/ci.yml +31 -0
- package/.mcp.json +12 -0
- package/.prettierignore +5 -0
- package/.prettierrc.cjs +22 -0
- package/AGENTS.md +183 -0
- package/LICENSE +201 -0
- package/README.md +128 -0
- package/package.json +74 -0
- package/playground/.claude/settings.local.json +5 -0
- package/playground/astro.config.mjs +7 -0
- package/playground/node_modules/.bin/astro +21 -0
- package/playground/node_modules/.bin/rollup +21 -0
- package/playground/node_modules/.bin/tsc +21 -0
- package/playground/node_modules/.bin/tsserver +21 -0
- package/playground/node_modules/.bin/vite +21 -0
- package/playground/node_modules/.vite/_svelte_metadata.json +1 -0
- package/playground/node_modules/.vite/deps/@astrojs_svelte_client__js.js +80 -0
- package/playground/node_modules/.vite/deps/@astrojs_svelte_client__js.js.map +7 -0
- package/playground/node_modules/.vite/deps/_metadata.json +184 -0
- package/playground/node_modules/.vite/deps/astro___aria-query.js +6776 -0
- package/playground/node_modules/.vite/deps/astro___aria-query.js.map +7 -0
- package/playground/node_modules/.vite/deps/astro___axobject-query.js +3754 -0
- package/playground/node_modules/.vite/deps/astro___axobject-query.js.map +7 -0
- package/playground/node_modules/.vite/deps/astro___html-escaper.js +34 -0
- package/playground/node_modules/.vite/deps/astro___html-escaper.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-AJXJMYAF.js +0 -0
- package/playground/node_modules/.vite/deps/chunk-AJXJMYAF.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-ALJIOON6.js +1005 -0
- package/playground/node_modules/.vite/deps/chunk-ALJIOON6.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-BUSYA2B4.js +8 -0
- package/playground/node_modules/.vite/deps/chunk-BUSYA2B4.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-CNYJBM5F.js +21 -0
- package/playground/node_modules/.vite/deps/chunk-CNYJBM5F.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-DBPNBGEI.js +223 -0
- package/playground/node_modules/.vite/deps/chunk-DBPNBGEI.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-G3C2FXJT.js +204 -0
- package/playground/node_modules/.vite/deps/chunk-G3C2FXJT.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-GKDKFWC5.js +27 -0
- package/playground/node_modules/.vite/deps/chunk-GKDKFWC5.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-HNCLEOC5.js +4376 -0
- package/playground/node_modules/.vite/deps/chunk-HNCLEOC5.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-JICYXBFU.js +688 -0
- package/playground/node_modules/.vite/deps/chunk-JICYXBFU.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-KCUTL6DD.js +5099 -0
- package/playground/node_modules/.vite/deps/chunk-KCUTL6DD.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-ZP4UNCSN.js +23 -0
- package/playground/node_modules/.vite/deps/chunk-ZP4UNCSN.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-ZREFNRZZ.js +148 -0
- package/playground/node_modules/.vite/deps/chunk-ZREFNRZZ.js.map +7 -0
- package/playground/node_modules/.vite/deps/package.json +3 -0
- package/playground/node_modules/.vite/deps/smol-toml.js +843 -0
- package/playground/node_modules/.vite/deps/smol-toml.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte.js +55 -0
- package/playground/node_modules/.vite/deps/svelte.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte___clsx.js +9 -0
- package/playground/node_modules/.vite/deps/svelte___clsx.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_animate.js +57 -0
- package/playground/node_modules/.vite/deps/svelte_animate.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_attachments.js +15 -0
- package/playground/node_modules/.vite/deps/svelte_attachments.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_easing.js +67 -0
- package/playground/node_modules/.vite/deps/svelte_easing.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_events.js +11 -0
- package/playground/node_modules/.vite/deps/svelte_events.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal.js +5 -0
- package/playground/node_modules/.vite/deps/svelte_internal.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_client.js +402 -0
- package/playground/node_modules/.vite/deps/svelte_internal_client.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_disclose-version.js +10 -0
- package/playground/node_modules/.vite/deps/svelte_internal_disclose-version.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_async.js +8 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_async.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_legacy.js +8 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_legacy.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_tracing.js +8 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_tracing.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_legacy.js +35 -0
- package/playground/node_modules/.vite/deps/svelte_legacy.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_motion.js +545 -0
- package/playground/node_modules/.vite/deps/svelte_motion.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity.js +29 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity_window.js +127 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity_window.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_store.js +103 -0
- package/playground/node_modules/.vite/deps/svelte_store.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_transition.js +208 -0
- package/playground/node_modules/.vite/deps/svelte_transition.js.map +7 -0
- package/playground/package.json +16 -0
- package/playground/pnpm-lock.yaml +3167 -0
- package/playground/src/content/authors/jane-doe.json +8 -0
- package/playground/src/content/config/build.toml +2 -0
- package/playground/src/content/courses/web-fundamentals.json +29 -0
- package/playground/src/content/docs/advanced.mdx +6 -0
- package/playground/src/content/docs/intro.md +6 -0
- package/playground/src/content/guides/getting-started.mdx +6 -0
- package/playground/src/content/posts/hello-world.md +7 -0
- package/playground/src/content/products/t-shirt.json +16 -0
- package/playground/src/content/recipes/pancakes.mdoc +8 -0
- package/playground/src/content/settings/site.yml +2 -0
- package/playground/src/content.config.ts +198 -0
- package/playground/src/env.d.ts +1 -0
- package/playground/src/pages/index.astro +11 -0
- package/playground/src/pages/nebula.astro +14 -0
- package/pnpm-workspace.yaml +2 -0
- package/scripts/subset-icons.mjs +178 -0
- package/src/astro/index.ts +295 -0
- package/src/client/Admin.svelte +283 -0
- package/src/client/components/BackendPicker.svelte +291 -0
- package/src/client/components/DraftChip.svelte +46 -0
- package/src/client/components/MetadataForm.svelte +56 -0
- package/src/client/components/ThemeToggle.svelte +18 -0
- package/src/client/components/dialogs/DeleteDraftDialog.svelte +51 -0
- package/src/client/components/dialogs/FilenameDialog.svelte +129 -0
- package/src/client/components/editor/EditorPane.svelte +227 -0
- package/src/client/components/editor/EditorTabs.svelte +81 -0
- package/src/client/components/editor/EditorToolbar.svelte +131 -0
- package/src/client/components/editor/FormatSelector.svelte +66 -0
- package/src/client/components/editor/Toolbar.svelte +17 -0
- package/src/client/components/fields/ArrayField.svelte +339 -0
- package/src/client/components/fields/ArrayItem.svelte +325 -0
- package/src/client/components/fields/BooleanField.svelte +114 -0
- package/src/client/components/fields/DateField.svelte +82 -0
- package/src/client/components/fields/EnumField.svelte +74 -0
- package/src/client/components/fields/FieldWrapper.svelte +96 -0
- package/src/client/components/fields/NumberField.svelte +99 -0
- package/src/client/components/fields/ObjectField.svelte +121 -0
- package/src/client/components/fields/SchemaField.svelte +107 -0
- package/src/client/components/fields/StringField.svelte +104 -0
- package/src/client/components/sidebar/AdminSidebar.svelte +339 -0
- package/src/client/components/sidebar/AdminSidebarSort.svelte +123 -0
- package/src/client/css/a11y.css +14 -0
- package/src/client/css/btn.css +113 -0
- package/src/client/css/dialog.css +29 -0
- package/src/client/css/field-input.css +39 -0
- package/src/client/css/reset.css +59 -0
- package/src/client/css/theme.css +77 -0
- package/src/client/index.ts +1 -0
- package/src/client/js/drafts/merge.svelte.ts +121 -0
- package/src/client/js/drafts/ops.svelte.ts +227 -0
- package/src/client/js/drafts/storage.ts +108 -0
- package/src/client/js/drafts/workers/diff.ts +40 -0
- package/src/client/js/editor/editor.svelte.ts +343 -0
- package/src/client/js/editor/languages.ts +98 -0
- package/src/client/js/editor/link-wrap.ts +45 -0
- package/src/client/js/editor/markdown-shortcuts.ts +261 -0
- package/src/client/js/handlers/admin.ts +246 -0
- package/src/client/js/state/dialogs.svelte.ts +35 -0
- package/src/client/js/state/router.svelte.ts +156 -0
- package/src/client/js/state/schema.svelte.ts +140 -0
- package/src/client/js/state/state.svelte.ts +334 -0
- package/src/client/js/state/theme.svelte.ts +173 -0
- package/src/client/js/storage/adapter.ts +102 -0
- package/src/client/js/storage/client.ts +150 -0
- package/src/client/js/storage/db.ts +36 -0
- package/src/client/js/storage/fsa.ts +110 -0
- package/src/client/js/storage/github.ts +297 -0
- package/src/client/js/storage/storage.ts +83 -0
- package/src/client/js/storage/workers/frontmatter.ts +320 -0
- package/src/client/js/storage/workers/storage.ts +177 -0
- package/src/client/js/storage/workers/toml-parser.ts +106 -0
- package/src/client/js/storage/workers/yaml-parser.ts +132 -0
- package/src/client/js/utils/file-types.ts +192 -0
- package/src/client/js/utils/format.ts +16 -0
- package/src/client/js/utils/frontmatter.ts +38 -0
- package/src/client/js/utils/schema-utils.ts +295 -0
- package/src/client/js/utils/slug.ts +18 -0
- package/src/client/js/utils/sort.ts +84 -0
- package/src/client/js/utils/stable-stringify.ts +27 -0
- package/src/client/js/utils/url-utils.ts +38 -0
- package/src/types.ts +25 -0
- package/src/virtual.d.ts +22 -0
- package/svelte.config.js +4 -0
- package/tests/astro/build.test.ts +63 -0
- package/tests/astro/index.test.ts +689 -0
- package/tests/client/components/Admin.test.ts +446 -0
- package/tests/client/components/BackendPicker.test.ts +239 -0
- package/tests/client/components/DraftChip.test.ts +53 -0
- package/tests/client/components/MetadataForm.test.ts +164 -0
- package/tests/client/components/dialogs/DeleteDraftDialog.test.ts +91 -0
- package/tests/client/components/dialogs/FilenameDialog.test.ts +209 -0
- package/tests/client/components/dialogs/dialog-stubs.ts +19 -0
- package/tests/client/components/editor/EditorPane.test.ts +100 -0
- package/tests/client/components/editor/EditorTabs.test.ts +253 -0
- package/tests/client/components/editor/EditorToolbar.test.ts +252 -0
- package/tests/client/components/editor/fixtures.ts +31 -0
- package/tests/client/components/fields/ArrayField.test.ts +197 -0
- package/tests/client/components/fields/BooleanField.test.ts +206 -0
- package/tests/client/components/fields/DateField.test.ts +210 -0
- package/tests/client/components/fields/EnumField.test.ts +246 -0
- package/tests/client/components/fields/NumberField.test.ts +240 -0
- package/tests/client/components/fields/ObjectField.test.ts +157 -0
- package/tests/client/components/fields/SchemaField.test.ts +190 -0
- package/tests/client/components/fields/StringField.test.ts +223 -0
- package/tests/client/components/sidebar/AdminSidebar.test.ts +285 -0
- package/tests/client/components/sidebar/AdminSidebarSort.test.ts +135 -0
- package/tests/client/components/sidebar/sort-mock.ts +23 -0
- package/tests/client/js/drafts/fixtures.ts +22 -0
- package/tests/client/js/drafts/merge.test.ts +282 -0
- package/tests/client/js/drafts/ops.test.ts +658 -0
- package/tests/client/js/drafts/storage.test.ts +200 -0
- package/tests/client/js/drafts/workers/diff.test.ts +165 -0
- package/tests/client/js/editor/editor.test.ts +616 -0
- package/tests/client/js/editor/link-wrap.test.ts +225 -0
- package/tests/client/js/editor/markdown-shortcuts.test.ts +370 -0
- package/tests/client/js/handlers/admin.test.ts +467 -0
- package/tests/client/js/state/router.test.ts +619 -0
- package/tests/client/js/state/schema.test.ts +266 -0
- package/tests/client/js/state/state.test.ts +328 -0
- package/tests/client/js/storage/adapter.test.ts +115 -0
- package/tests/client/js/storage/client.test.ts +250 -0
- package/tests/client/js/storage/db.test.ts +59 -0
- package/tests/client/js/storage/fsa.test.ts +284 -0
- package/tests/client/js/storage/github.test.ts +349 -0
- package/tests/client/js/storage/mock-port.ts +95 -0
- package/tests/client/js/storage/storage.test.ts +77 -0
- package/tests/client/js/storage/workers/frontmatter.test.ts +479 -0
- package/tests/client/js/storage/workers/storage.test.ts +299 -0
- package/tests/client/js/storage/workers/toml-parser.test.ts +169 -0
- package/tests/client/js/storage/workers/yaml-parser.test.ts +168 -0
- package/tests/client/js/utils/file-types.test.ts +268 -0
- package/tests/client/js/utils/frontmatter.test.ts +87 -0
- package/tests/client/js/utils/schema-utils.test.ts +318 -0
- package/tests/client/js/utils/slug.test.ts +58 -0
- package/tests/client/js/utils/sort.test.ts +276 -0
- package/tests/client/js/utils/stable-stringify.test.ts +68 -0
- package/tests/client/js/utils/url-utils.test.ts +70 -0
- package/tests/e2e/backend-connection.test.ts +301 -0
- package/tests/e2e/draft-lifecycle.test.ts +388 -0
- package/tests/e2e/editing.test.ts +355 -0
- package/tests/e2e/github-adapter.test.ts +330 -0
- package/tests/e2e/helpers/mock-adapter.ts +166 -0
- package/tests/e2e/helpers/test-app.ts +155 -0
- package/tests/e2e/navigation.test.ts +358 -0
- package/tests/e2e/publishing.test.ts +345 -0
- package/tests/e2e/unsaved-changes.test.ts +317 -0
- package/tests/setup.ts +2 -0
- package/tests/stubs/codemirror.ts +197 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +178 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Backend configuration persistence using IndexedDB.
|
|
3
|
+
* Stores and retrieves the active storage backend (FSA or GitHub).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { openDB } from './db';
|
|
7
|
+
|
|
8
|
+
// Fixed key for the backend config
|
|
9
|
+
const BACKEND_KEY = 'backend';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Backend configuration stored in IndexedDB. Tagged union discriminated on `type`.
|
|
13
|
+
* Security note: the GitHub token is stored in plaintext in IndexedDB. This is a
|
|
14
|
+
* deliberate trade-off for a client-only app with no server to proxy through.
|
|
15
|
+
* Same-origin policy protects it from other sites, but any XSS vulnerability
|
|
16
|
+
* would expose the token.
|
|
17
|
+
*/
|
|
18
|
+
export type BackendConfig =
|
|
19
|
+
| { type: 'fsa'; handle: FileSystemDirectoryHandle }
|
|
20
|
+
| { type: 'github'; token: string; repo: string };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Stores backend configuration in IndexedDB for persistence across sessions.
|
|
24
|
+
* @param {BackendConfig} config - The backend config to store
|
|
25
|
+
* @return {Promise<void>}
|
|
26
|
+
*/
|
|
27
|
+
export async function saveBackend(config: BackendConfig): Promise<void> {
|
|
28
|
+
const db = await openDB();
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const tx = db.transaction('handles', 'readwrite');
|
|
31
|
+
tx.objectStore('handles').put(config, BACKEND_KEY);
|
|
32
|
+
tx.oncomplete = () => resolve();
|
|
33
|
+
tx.onerror = () => reject(tx.error);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Retrieves stored backend configuration from IndexedDB. Handles migration from the old format where a bare FileSystemDirectoryHandle was stored.
|
|
39
|
+
* @return {Promise<BackendConfig | null>} The stored config, or null if none exists
|
|
40
|
+
*/
|
|
41
|
+
export async function loadBackend(): Promise<BackendConfig | null> {
|
|
42
|
+
const db = await openDB();
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const tx = db.transaction('handles', 'readonly');
|
|
45
|
+
const request = tx.objectStore('handles').get(BACKEND_KEY);
|
|
46
|
+
request.onsuccess = () => {
|
|
47
|
+
const result = request.result;
|
|
48
|
+
if (!result) {
|
|
49
|
+
// Check for old-format handle stored under the legacy key
|
|
50
|
+
const legacyRequest = tx.objectStore('handles').get('projectRoot');
|
|
51
|
+
legacyRequest.onsuccess = () => {
|
|
52
|
+
const legacy = legacyRequest.result;
|
|
53
|
+
if (legacy && legacy instanceof FileSystemDirectoryHandle) {
|
|
54
|
+
resolve({ type: 'fsa', handle: legacy });
|
|
55
|
+
} else {
|
|
56
|
+
resolve(null);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
legacyRequest.onerror = () => resolve(null);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
resolve(result as BackendConfig);
|
|
63
|
+
};
|
|
64
|
+
request.onerror = () => reject(request.error);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Removes the stored backend configuration from IndexedDB. Also clears the legacy key if present.
|
|
70
|
+
* @return {Promise<void>}
|
|
71
|
+
*/
|
|
72
|
+
export async function clearBackend(): Promise<void> {
|
|
73
|
+
const db = await openDB();
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const tx = db.transaction('handles', 'readwrite');
|
|
76
|
+
const store = tx.objectStore('handles');
|
|
77
|
+
store.delete(BACKEND_KEY);
|
|
78
|
+
// Clean up legacy key if present
|
|
79
|
+
store.delete('projectRoot');
|
|
80
|
+
tx.oncomplete = () => resolve();
|
|
81
|
+
tx.onerror = () => reject(tx.error);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Orchestrator Worker
|
|
3
|
+
*
|
|
4
|
+
* Routes file parsing by category: frontmatter files have their YAML block
|
|
5
|
+
* extracted via string manipulation and sent to the YAML parser worker;
|
|
6
|
+
* JSON data files are parsed inline; YAML/TOML data files are sent to their
|
|
7
|
+
* respective parser workers. Parser workers are lazily spawned on first need.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { StorageClient } from '../client';
|
|
11
|
+
import { getFileCategory, getDataFormat } from '../../utils/file-types';
|
|
12
|
+
import { splitFrontmatter } from '../../utils/frontmatter';
|
|
13
|
+
import type { FileEntry } from '../adapter';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extracts the raw YAML block from a frontmatter-delimited file (markdown/MDX/Markdoc).
|
|
17
|
+
* Delegates BOM stripping, CRLF normalization, and delimiter logic to splitFrontmatter.
|
|
18
|
+
* Returns the raw YAML string without parsing it — parsing is delegated to the YAML parser worker.
|
|
19
|
+
* @param {string} content - Raw file content
|
|
20
|
+
* @return {string | null} The raw YAML string between --- delimiters, or null if none found
|
|
21
|
+
*/
|
|
22
|
+
function extractYamlBlock(content: string): string | null {
|
|
23
|
+
const { rawFrontmatter } = splitFrontmatter(content);
|
|
24
|
+
return rawFrontmatter.trim() ? rawFrontmatter : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
//////////////////////////////
|
|
29
|
+
// Batch item type for parser worker communication
|
|
30
|
+
//////////////////////////////
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// A key/content pair for batch parsing requests.
|
|
34
|
+
type BatchItem = {
|
|
35
|
+
key: string;
|
|
36
|
+
content: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
//////////////////////////////
|
|
41
|
+
// Parser worker management
|
|
42
|
+
//////////////////////////////
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
// Lazily-spawned parser workers
|
|
46
|
+
let yamlWorker: Worker | null = null;
|
|
47
|
+
let tomlWorker: Worker | null = null;
|
|
48
|
+
|
|
49
|
+
// Incrementing ID for correlating batch requests with responses
|
|
50
|
+
let batchIdCounter = 0;
|
|
51
|
+
|
|
52
|
+
// Pending batch response promises keyed by ID
|
|
53
|
+
const pendingBatches = new Map<
|
|
54
|
+
string,
|
|
55
|
+
{
|
|
56
|
+
resolve: (results: Record<string, Record<string, unknown>>) => void;
|
|
57
|
+
reject: (err: Error) => void;
|
|
58
|
+
}
|
|
59
|
+
>();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns the lazily-spawned YAML parser worker, creating it on first call.
|
|
63
|
+
* Uses `.js` extension in the URL because svelte-package doesn't rewrite URL strings.
|
|
64
|
+
* @return {Worker} The YAML parser worker instance
|
|
65
|
+
*/
|
|
66
|
+
function getYamlWorker(): Worker {
|
|
67
|
+
if (!yamlWorker) {
|
|
68
|
+
yamlWorker = new Worker(new URL('./yaml-parser.js', import.meta.url), {
|
|
69
|
+
type: 'module',
|
|
70
|
+
});
|
|
71
|
+
yamlWorker.onmessage = handleParserResponse;
|
|
72
|
+
yamlWorker.onerror = handleWorkerError;
|
|
73
|
+
}
|
|
74
|
+
return yamlWorker;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returns the lazily-spawned TOML parser worker, creating it on first call.
|
|
79
|
+
* Uses `.js` extension in the URL because svelte-package doesn't rewrite URL strings.
|
|
80
|
+
* @return {Worker} The TOML parser worker instance
|
|
81
|
+
*/
|
|
82
|
+
function getTomlWorker(): Worker {
|
|
83
|
+
if (!tomlWorker) {
|
|
84
|
+
tomlWorker = new Worker(new URL('./toml-parser.js', import.meta.url), {
|
|
85
|
+
type: 'module',
|
|
86
|
+
});
|
|
87
|
+
tomlWorker.onmessage = handleParserResponse;
|
|
88
|
+
tomlWorker.onerror = handleWorkerError;
|
|
89
|
+
}
|
|
90
|
+
return tomlWorker;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Handles fatal errors from parser workers by rejecting all pending batch promises
|
|
95
|
+
* so callers don't hang indefinitely waiting for a response that will never arrive.
|
|
96
|
+
* @param {ErrorEvent} e - The error event from the worker
|
|
97
|
+
* @return {void}
|
|
98
|
+
*/
|
|
99
|
+
function handleWorkerError(e: ErrorEvent): void {
|
|
100
|
+
for (const [id, { reject }] of pendingBatches) {
|
|
101
|
+
reject(new Error(`Parser worker error: ${e.message}`));
|
|
102
|
+
pendingBatches.delete(id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handles responses from parser workers by resolving/rejecting the corresponding
|
|
108
|
+
* pending batch promise based on the response ID.
|
|
109
|
+
* @param {MessageEvent} event - The message event from the parser worker
|
|
110
|
+
* @return {void}
|
|
111
|
+
*/
|
|
112
|
+
function handleParserResponse(event: MessageEvent): void {
|
|
113
|
+
const { type, id, ok, results, error } = event.data;
|
|
114
|
+
// Ignore messages that aren't batch results (e.g. single parse results, stringify results)
|
|
115
|
+
if (type !== 'parse-batch-result') return;
|
|
116
|
+
const pending = pendingBatches.get(id);
|
|
117
|
+
if (!pending) return;
|
|
118
|
+
pendingBatches.delete(id);
|
|
119
|
+
|
|
120
|
+
if (ok) {
|
|
121
|
+
pending.resolve(results);
|
|
122
|
+
} else {
|
|
123
|
+
pending.reject(new Error(error));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Sends a batch of key/content pairs to a parser worker and returns a promise
|
|
129
|
+
* that resolves with the parsed results map. Uses an incrementing ID for
|
|
130
|
+
* request/response correlation, following the same pattern as StorageClient.
|
|
131
|
+
* @param {Worker} worker - The parser worker to send the batch to
|
|
132
|
+
* @param {BatchItem[]} items - Array of key/content pairs to parse
|
|
133
|
+
* @return {Promise<Record<string, Record<string, unknown>>>} Map of key to parsed data
|
|
134
|
+
*/
|
|
135
|
+
function sendBatch(
|
|
136
|
+
worker: Worker,
|
|
137
|
+
items: BatchItem[],
|
|
138
|
+
): Promise<Record<string, Record<string, unknown>>> {
|
|
139
|
+
const id = String(++batchIdCounter);
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
pendingBatches.set(id, { resolve, reject });
|
|
142
|
+
worker.postMessage({ type: 'parse-batch', id, items });
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/*
|
|
147
|
+
//////////////////////////////
|
|
148
|
+
// File categorization and parsing
|
|
149
|
+
//////////////////////////////
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Processes a list of files by categorizing each file, collecting batch items
|
|
154
|
+
* for YAML/TOML parser workers, parsing JSON inline, and assembling the final
|
|
155
|
+
* items array with parsed data.
|
|
156
|
+
* @param {FileEntry[]} files - The files returned by the storage adapter
|
|
157
|
+
* @return {Promise<Array<{ filename: string; data: Record<string, unknown> }>>} Parsed items
|
|
158
|
+
*/
|
|
159
|
+
async function processFiles(
|
|
160
|
+
files: FileEntry[],
|
|
161
|
+
): Promise<Array<{ filename: string; data: Record<string, unknown> }>> {
|
|
162
|
+
// Inline-parsed results (JSON files)
|
|
163
|
+
const inlineResults: Array<{
|
|
164
|
+
filename: string;
|
|
165
|
+
data: Record<string, unknown>;
|
|
166
|
+
}> = [];
|
|
167
|
+
|
|
168
|
+
// Batch items for YAML worker (frontmatter YAML blocks + YAML data files)
|
|
169
|
+
const yamlBatch: BatchItem[] = [];
|
|
170
|
+
// Batch items for TOML worker
|
|
171
|
+
const tomlBatch: BatchItem[] = [];
|
|
172
|
+
// Files with no frontmatter get empty data
|
|
173
|
+
const emptyDataFiles: string[] = [];
|
|
174
|
+
|
|
175
|
+
for (const file of files) {
|
|
176
|
+
const category = getFileCategory(file.filename);
|
|
177
|
+
|
|
178
|
+
if (category === 'frontmatter') {
|
|
179
|
+
const yamlBlock = extractYamlBlock(file.content);
|
|
180
|
+
if (yamlBlock) {
|
|
181
|
+
yamlBatch.push({ key: file.filename, content: yamlBlock });
|
|
182
|
+
} else {
|
|
183
|
+
// No frontmatter found — include with empty data
|
|
184
|
+
emptyDataFiles.push(file.filename);
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (category === 'data') {
|
|
190
|
+
const format = getDataFormat(file.filename);
|
|
191
|
+
|
|
192
|
+
if (format === 'json') {
|
|
193
|
+
try {
|
|
194
|
+
const data = JSON.parse(file.content) as Record<string, unknown>;
|
|
195
|
+
inlineResults.push({ filename: file.filename, data });
|
|
196
|
+
} catch {
|
|
197
|
+
// Invalid JSON — include with empty data
|
|
198
|
+
inlineResults.push({ filename: file.filename, data: {} });
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (format === 'yaml') {
|
|
204
|
+
yamlBatch.push({ key: file.filename, content: file.content });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (format === 'toml') {
|
|
209
|
+
tomlBatch.push({ key: file.filename, content: file.content });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Unrecognised file type — include with empty data
|
|
215
|
+
emptyDataFiles.push(file.filename);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Send batches to parser workers in parallel
|
|
219
|
+
const promises: Promise<Record<string, Record<string, unknown>>>[] = [];
|
|
220
|
+
let yamlPromiseIdx = -1;
|
|
221
|
+
let tomlPromiseIdx = -1;
|
|
222
|
+
|
|
223
|
+
if (yamlBatch.length > 0) {
|
|
224
|
+
yamlPromiseIdx = promises.length;
|
|
225
|
+
promises.push(sendBatch(getYamlWorker(), yamlBatch));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (tomlBatch.length > 0) {
|
|
229
|
+
tomlPromiseIdx = promises.length;
|
|
230
|
+
promises.push(sendBatch(getTomlWorker(), tomlBatch));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const batchResults = await Promise.all(promises);
|
|
234
|
+
|
|
235
|
+
// Assemble final items from all sources
|
|
236
|
+
const items: Array<{ filename: string; data: Record<string, unknown> }> = [];
|
|
237
|
+
|
|
238
|
+
// Add inline results (JSON)
|
|
239
|
+
items.push(...inlineResults);
|
|
240
|
+
|
|
241
|
+
// Add YAML results — filenames are derived from the batch result keys
|
|
242
|
+
if (yamlPromiseIdx >= 0) {
|
|
243
|
+
const yamlResults = batchResults[yamlPromiseIdx];
|
|
244
|
+
for (const [filename, data] of Object.entries(yamlResults)) {
|
|
245
|
+
items.push({ filename, data });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Add TOML results — filenames are derived from the batch result keys
|
|
250
|
+
if (tomlPromiseIdx >= 0) {
|
|
251
|
+
const tomlResults = batchResults[tomlPromiseIdx];
|
|
252
|
+
for (const [filename, data] of Object.entries(tomlResults)) {
|
|
253
|
+
items.push({ filename, data });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Add empty-data files
|
|
258
|
+
for (const filename of emptyDataFiles) {
|
|
259
|
+
items.push({ filename, data: {} });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return items;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/*
|
|
266
|
+
//////////////////////////////
|
|
267
|
+
// Main message handler
|
|
268
|
+
//////////////////////////////
|
|
269
|
+
*/
|
|
270
|
+
|
|
271
|
+
// Storage client, initialized when the main thread transfers a port
|
|
272
|
+
let storageClient: StorageClient | null = null;
|
|
273
|
+
|
|
274
|
+
// Handle messages from main thread
|
|
275
|
+
self.addEventListener('message', async (event) => {
|
|
276
|
+
const { type } = event.data;
|
|
277
|
+
|
|
278
|
+
if (type === 'port') {
|
|
279
|
+
// Main thread is transferring a MessagePort connected to the storage SharedWorker
|
|
280
|
+
const port = event.ports[0];
|
|
281
|
+
storageClient = new StorageClient(port);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (type === 'parse') {
|
|
286
|
+
const { collection } = event.data;
|
|
287
|
+
if (!storageClient) {
|
|
288
|
+
self.postMessage({
|
|
289
|
+
type: 'error',
|
|
290
|
+
message: 'Storage port not initialized',
|
|
291
|
+
});
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
// Pass extensions from the message, defaulting to markdown for backward compatibility
|
|
297
|
+
const extensions: string[] = event.data.extensions ?? ['.md', '.mdx'];
|
|
298
|
+
const files: FileEntry[] = await storageClient.listFiles(
|
|
299
|
+
collection,
|
|
300
|
+
extensions,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const items = await processFiles(files);
|
|
304
|
+
|
|
305
|
+
// Sort alphabetically by title, falling back to filename
|
|
306
|
+
items.sort((a, b) => {
|
|
307
|
+
const aTitle =
|
|
308
|
+
typeof a.data.title === 'string' ? a.data.title : a.filename;
|
|
309
|
+
const bTitle =
|
|
310
|
+
typeof b.data.title === 'string' ? b.data.title : b.filename;
|
|
311
|
+
return aTitle.toLowerCase().localeCompare(bTitle.toLowerCase());
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
self.postMessage({ type: 'result', collection, items });
|
|
315
|
+
} catch (err) {
|
|
316
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
317
|
+
self.postMessage({ type: 'error', message });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SharedWorker entry point for storage operations.
|
|
3
|
+
* Receives requests over MessagePort, dispatches to the active StorageAdapter,
|
|
4
|
+
* and posts responses back to each connected client.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
StorageAdapter,
|
|
9
|
+
StorageRequest,
|
|
10
|
+
StorageResponse,
|
|
11
|
+
} from '../adapter';
|
|
12
|
+
|
|
13
|
+
// The single active adapter instance shared across all connected ports
|
|
14
|
+
let adapter: StorageAdapter | null = null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Handles a request message and returns the appropriate response.
|
|
18
|
+
* @param {StorageRequest} msg - The incoming request message
|
|
19
|
+
* @return {Promise<StorageResponse>} The response to send back
|
|
20
|
+
*/
|
|
21
|
+
async function handleMessage(msg: StorageRequest): Promise<StorageResponse> {
|
|
22
|
+
switch (msg.type) {
|
|
23
|
+
case 'init': {
|
|
24
|
+
try {
|
|
25
|
+
if (msg.backend.type === 'fsa') {
|
|
26
|
+
const { FsaAdapter } = await import('../fsa');
|
|
27
|
+
adapter = new FsaAdapter(msg.backend.handle);
|
|
28
|
+
} else {
|
|
29
|
+
const { GitHubAdapter } = await import('../github');
|
|
30
|
+
const gh = new GitHubAdapter(msg.backend.token, msg.backend.repo);
|
|
31
|
+
await gh.validate();
|
|
32
|
+
adapter = gh;
|
|
33
|
+
}
|
|
34
|
+
return { type: 'init', ok: true };
|
|
35
|
+
} catch (err) {
|
|
36
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
37
|
+
return { type: 'init', ok: false, error };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
case 'listFiles': {
|
|
42
|
+
if (!adapter)
|
|
43
|
+
return {
|
|
44
|
+
type: 'listFiles',
|
|
45
|
+
ok: false,
|
|
46
|
+
error: 'No backend initialized',
|
|
47
|
+
};
|
|
48
|
+
try {
|
|
49
|
+
const files = await adapter.listFiles(msg.collection, msg.extensions);
|
|
50
|
+
return { type: 'listFiles', ok: true, files };
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return {
|
|
53
|
+
type: 'listFiles',
|
|
54
|
+
ok: false,
|
|
55
|
+
error: err instanceof Error ? err.message : String(err),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case 'readFile': {
|
|
61
|
+
if (!adapter)
|
|
62
|
+
return { type: 'readFile', ok: false, error: 'No backend initialized' };
|
|
63
|
+
try {
|
|
64
|
+
const content = await adapter.readFile(msg.collection, msg.filename);
|
|
65
|
+
return { type: 'readFile', ok: true, content };
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return {
|
|
68
|
+
type: 'readFile',
|
|
69
|
+
ok: false,
|
|
70
|
+
error: err instanceof Error ? err.message : String(err),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case 'writeFile': {
|
|
76
|
+
if (!adapter)
|
|
77
|
+
return {
|
|
78
|
+
type: 'writeFile',
|
|
79
|
+
ok: false,
|
|
80
|
+
error: 'No backend initialized',
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
await adapter.writeFile(msg.collection, msg.filename, msg.content);
|
|
84
|
+
return { type: 'writeFile', ok: true };
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return {
|
|
87
|
+
type: 'writeFile',
|
|
88
|
+
ok: false,
|
|
89
|
+
error: err instanceof Error ? err.message : String(err),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case 'writeFiles': {
|
|
95
|
+
if (!adapter)
|
|
96
|
+
return {
|
|
97
|
+
type: 'writeFiles',
|
|
98
|
+
ok: false,
|
|
99
|
+
error: 'No backend initialized',
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
await adapter.writeFiles(msg.files);
|
|
103
|
+
return { type: 'writeFiles', ok: true };
|
|
104
|
+
} catch (err) {
|
|
105
|
+
return {
|
|
106
|
+
type: 'writeFiles',
|
|
107
|
+
ok: false,
|
|
108
|
+
error: err instanceof Error ? err.message : String(err),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case 'deleteFile': {
|
|
114
|
+
if (!adapter)
|
|
115
|
+
return {
|
|
116
|
+
type: 'deleteFile',
|
|
117
|
+
ok: false,
|
|
118
|
+
error: 'No backend initialized',
|
|
119
|
+
};
|
|
120
|
+
try {
|
|
121
|
+
await adapter.deleteFile(msg.collection, msg.filename);
|
|
122
|
+
return { type: 'deleteFile', ok: true };
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return {
|
|
125
|
+
type: 'deleteFile',
|
|
126
|
+
ok: false,
|
|
127
|
+
error: err instanceof Error ? err.message : String(err),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case 'teardown': {
|
|
133
|
+
adapter = null;
|
|
134
|
+
return { type: 'teardown', ok: true };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
default: {
|
|
138
|
+
// Prevents silent hangs if an unrecognized message type arrives
|
|
139
|
+
const exhaustive: never = msg;
|
|
140
|
+
return {
|
|
141
|
+
type: (exhaustive as any).type,
|
|
142
|
+
ok: false,
|
|
143
|
+
error: 'Unknown message type',
|
|
144
|
+
} as StorageResponse;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Wires up message handling for a connected port. Passes through `_id` from requests so the StorageClient can correlate responses.
|
|
151
|
+
* @param {MessagePort} port - The port to listen on
|
|
152
|
+
* @return {void}
|
|
153
|
+
*/
|
|
154
|
+
function setupPort(port: MessagePort): void {
|
|
155
|
+
port.addEventListener('message', async (event) => {
|
|
156
|
+
const { _id, ...msg } = event.data;
|
|
157
|
+
|
|
158
|
+
// Handle port bridging — main thread transfers ports for dedicated workers
|
|
159
|
+
if (msg.type === 'connect-port') {
|
|
160
|
+
const transferredPort = event.ports[0];
|
|
161
|
+
if (transferredPort) setupPort(transferredPort);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const response = await handleMessage(msg as StorageRequest);
|
|
166
|
+
port.postMessage({ ...response, _id });
|
|
167
|
+
});
|
|
168
|
+
port.start();
|
|
169
|
+
// Acknowledge the connection so the caller knows the port is ready
|
|
170
|
+
port.postMessage({ type: 'port-connected' });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// SharedWorker entry — handle direct connections from Window contexts
|
|
174
|
+
self.addEventListener('connect', (event: MessageEvent) => {
|
|
175
|
+
const port = (event as any).ports[0] as MessagePort;
|
|
176
|
+
setupPort(port);
|
|
177
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* TOML parser worker
|
|
3
|
+
*
|
|
4
|
+
* Runs as a dedicated Worker. Handles three message types — parse, parse-batch,
|
|
5
|
+
* stringify — and posts results back to the main thread via self.postMessage.
|
|
6
|
+
* All handlers are wrapped in try/catch so errors are returned as structured
|
|
7
|
+
* failure messages rather than unhandled rejections.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { parse, stringify } from 'smol-toml';
|
|
11
|
+
|
|
12
|
+
// Inbound message shape for a single TOML parse request.
|
|
13
|
+
interface ParseMessage {
|
|
14
|
+
type: 'parse';
|
|
15
|
+
id: string;
|
|
16
|
+
content: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// A single item in a batch parse request.
|
|
20
|
+
interface BatchItem {
|
|
21
|
+
key: string;
|
|
22
|
+
content: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Inbound message shape for a batch TOML parse request.
|
|
26
|
+
interface ParseBatchMessage {
|
|
27
|
+
type: 'parse-batch';
|
|
28
|
+
id: string;
|
|
29
|
+
items: BatchItem[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Inbound message shape for a TOML stringify request.
|
|
33
|
+
interface StringifyMessage {
|
|
34
|
+
type: 'stringify';
|
|
35
|
+
id: string;
|
|
36
|
+
data: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Union of all inbound message types.
|
|
40
|
+
type InboundMessage = ParseMessage | ParseBatchMessage | StringifyMessage;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Handles a 'parse' request: parses a single TOML string and posts the result.
|
|
44
|
+
* @param {ParseMessage} msg - The inbound parse message
|
|
45
|
+
* @return {void}
|
|
46
|
+
*/
|
|
47
|
+
function handleParse(msg: ParseMessage): void {
|
|
48
|
+
const { id, content } = msg;
|
|
49
|
+
try {
|
|
50
|
+
const data = parse(content) as Record<string, unknown>;
|
|
51
|
+
self.postMessage({ type: 'parse-result', id, ok: true, data });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
54
|
+
self.postMessage({ type: 'parse-result', id, ok: false, error });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Handles a 'parse-batch' request: parses multiple TOML strings keyed by
|
|
60
|
+
* a caller-supplied key, and posts a single result containing all parsed values.
|
|
61
|
+
* @param {ParseBatchMessage} msg - The inbound parse-batch message
|
|
62
|
+
* @return {void}
|
|
63
|
+
*/
|
|
64
|
+
function handleParseBatch(msg: ParseBatchMessage): void {
|
|
65
|
+
const { id, items } = msg;
|
|
66
|
+
try {
|
|
67
|
+
const results: Record<string, Record<string, unknown>> = {};
|
|
68
|
+
for (const item of items) {
|
|
69
|
+
results[item.key] = parse(item.content) as Record<string, unknown>;
|
|
70
|
+
}
|
|
71
|
+
self.postMessage({ type: 'parse-batch-result', id, ok: true, results });
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
74
|
+
self.postMessage({ type: 'parse-batch-result', id, ok: false, error });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Handles a 'stringify' request: serializes a plain object to a TOML string
|
|
80
|
+
* and posts the result.
|
|
81
|
+
* @param {StringifyMessage} msg - The inbound stringify message
|
|
82
|
+
* @return {void}
|
|
83
|
+
*/
|
|
84
|
+
function handleStringify(msg: StringifyMessage): void {
|
|
85
|
+
const { id, data } = msg;
|
|
86
|
+
try {
|
|
87
|
+
const content = stringify(data);
|
|
88
|
+
self.postMessage({ type: 'stringify-result', id, ok: true, content });
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
91
|
+
self.postMessage({ type: 'stringify-result', id, ok: false, error });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Dispatch incoming messages by type
|
|
96
|
+
self.addEventListener('message', (event: MessageEvent<InboundMessage>) => {
|
|
97
|
+
const msg = event.data;
|
|
98
|
+
|
|
99
|
+
if (msg.type === 'parse') {
|
|
100
|
+
handleParse(msg);
|
|
101
|
+
} else if (msg.type === 'parse-batch') {
|
|
102
|
+
handleParseBatch(msg);
|
|
103
|
+
} else if (msg.type === 'stringify') {
|
|
104
|
+
handleStringify(msg);
|
|
105
|
+
}
|
|
106
|
+
});
|