@dvina/widget-kit 0.0.33 → 0.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/compose.d.ts CHANGED
@@ -7,3 +7,7 @@ export declare function composeWidgetPage(input: { kind: WidgetKind; title: stri
7
7
  export declare function widgetPageKind(page: string): WidgetKind | null;
8
8
  /** The widget's own code from a composed page; the text itself for anything else. */
9
9
  export declare function widgetSource(page: string): string;
10
+ /** The data a downloaded widget opens with: the reader's own, as it was when they downloaded it. */
11
+ export type WidgetSeedData = { artifactId?: string; version: number; collections: Record<string, Record<string, unknown>> };
12
+ /** A composed page with its seed, for a download; a fresh file id unless one is given. Throws for any other page. */
13
+ export declare function seedWidgetPage(page: string, data: WidgetSeedData, fileId?: string): string;
package/dist/compose.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  kitReactScript,
5
5
  kitScript,
6
6
  reactLoaderScript,
7
+ standaloneScript,
7
8
  tailwindCss,
8
9
  tailwindScript,
9
10
  tokensCss
@@ -32,6 +33,8 @@ var END = "<!-- /dvina:widget -->";
32
33
  var escapeHtml = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
33
34
  var inScript = (text) => text.replace(/<\/(script|style)/gi, "<\\/$1").replace(/<!--/g, "<\\!--");
34
35
  var literal = (value) => JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
36
+ var STANDALONE_ID = "dvina-standalone";
37
+ var standalone = () => `<script id="${STANDALONE_ID}">${inScript(standaloneScript)}</script>`;
35
38
  function head(kind, title, extra = "") {
36
39
  return [
37
40
  '<meta charset="utf-8" />',
@@ -41,6 +44,7 @@ function head(kind, title, extra = "") {
41
44
  `<style id="dvina-tokens">${inScript(tokensCss)}</style>`,
42
45
  `<style id="dvina-kit">${inScript(kitCss)}</style>`,
43
46
  `<script id="dvina-kit-script">${inScript(kitScript)}</script>`,
47
+ standalone(),
44
48
  extra
45
49
  ].join("");
46
50
  }
@@ -74,9 +78,26 @@ function widgetSource(page) {
74
78
  const code = page.slice(start + START.length, end).replace(/^\n/, "").replace(/\n$/, "");
75
79
  return widgetPageKind(page) === "react" ? code.replace(/<\\\/(script|style)/gi, "</$1").replace(/<\\!--/g, "<!--") : code;
76
80
  }
81
+ var SEED = /<script type="application\/json" id="dvina-seed">[\s\S]*?<\/script>/;
82
+ function seedWidgetPage(page, data, fileId = crypto.randomUUID()) {
83
+ if (widgetPageKind(page) === null) throw new Error("Only a composed widget page takes a seed.");
84
+ const seed = {
85
+ fileId,
86
+ ...data.artifactId ? { artifactId: data.artifactId } : {},
87
+ version: data.version,
88
+ collections: data.collections
89
+ };
90
+ const element = `<script type="application/json" id="dvina-seed">${literal(seed)}</script>`;
91
+ const end = page.search(/<\/head>/i);
92
+ if (end < 0) throw new Error("The page has no head to seed.");
93
+ const head2 = page.slice(0, end).replace(SEED, "");
94
+ const addition = head2.includes(`id="${STANDALONE_ID}"`) ? element : element + standalone();
95
+ return head2 + addition + page.slice(end);
96
+ }
77
97
  export {
78
98
  WIDGET_PAGE_META,
79
99
  composeWidgetPage,
100
+ seedWidgetPage,
80
101
  widgetPageKind,
81
102
  widgetSource
82
103
  };
package/dist/db.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
2
+ export type Doc = { [key: string]: JsonValue };
3
+ export type Collections = Record<string, Record<string, Doc>>;
4
+ export type Snapshot = { version: number; collections: Collections };
5
+ export type Op = { op: "set" | "update" | "delete"; collection: string; id: string; doc?: Doc };
6
+ export type Changed = { collection: string; id: string; doc: Doc | null };
7
+ /** How long writes gather before they go to the transport as one batch. */
8
+ export declare const WRITE_BATCH_MS: number;
9
+ export interface DbTransport {
10
+ read(): Promise<Snapshot>;
11
+ apply(ops: Op[]): Promise<{ version: number; changed: Changed[] }>;
12
+ }
13
+ export type ListOptions = {
14
+ where?: Doc | ((doc: Doc & { id: string }) => boolean);
15
+ orderBy?: string | [string, "asc" | "desc"];
16
+ limit?: number;
17
+ };
18
+ export interface Collection {
19
+ get(id: string): Promise<(Doc & { id: string }) | null>;
20
+ list(options?: ListOptions): Promise<Array<Doc & { id: string }>>;
21
+ set(id: string, doc: Doc): Promise<void>;
22
+ update(id: string, patch: Doc): Promise<void>;
23
+ delete(id: string): Promise<void>;
24
+ subscribe(listener: () => void): () => void;
25
+ }
26
+ export interface Db {
27
+ collection(name: string): Collection;
28
+ }
29
+ export interface KeyValueStorage {
30
+ get(key: string): Promise<JsonValue | undefined>;
31
+ set(key: string, value: JsonValue): Promise<void>;
32
+ delete(key: string): Promise<void>;
33
+ list(options?: { prefix?: string }): Promise<string[]>;
34
+ }
35
+ /** `dvina.use('db')` and `dvina.use('storage')` over one transport. */
36
+ export declare function createDb(transport: DbTransport, onError?: (error: Error) => void): { db: Db; storage: KeyValueStorage };
package/dist/db.js ADDED
@@ -0,0 +1,156 @@
1
+ // src/runtime/db.ts
2
+ var WRITE_BATCH_MS = 300;
3
+ var NAME = /^[A-Za-z0-9_.:-]{1,128}$/;
4
+ function checkName(kind, value) {
5
+ if (typeof value !== "string" || !NAME.test(value)) {
6
+ throw new Error(`A ${kind} is 1\u2013128 letters, digits or . _ : - ("${String(value)}" is not).`);
7
+ }
8
+ }
9
+ function compare(a, b) {
10
+ if (a === b) return 0;
11
+ if (a === void 0 || a === null) return 1;
12
+ if (b === void 0 || b === null) return -1;
13
+ return a < b ? -1 : 1;
14
+ }
15
+ function createStore(transport, onError) {
16
+ let snapshot = null;
17
+ let loading = null;
18
+ let queue = [];
19
+ let timer = 0;
20
+ let inFlight = false;
21
+ const listeners = /* @__PURE__ */ new Set();
22
+ const notify = () => listeners.forEach((listener) => listener());
23
+ function load(force = false) {
24
+ if (snapshot && !force) return Promise.resolve(snapshot);
25
+ if (loading && !force) return loading;
26
+ loading = transport.read().then(
27
+ (read) => {
28
+ loading = null;
29
+ snapshot = { version: read.version, collections: structuredClone(read.collections) };
30
+ for (const { op } of queue) applyLocally(op);
31
+ notify();
32
+ return snapshot;
33
+ },
34
+ (error) => {
35
+ loading = null;
36
+ throw error;
37
+ }
38
+ );
39
+ return loading;
40
+ }
41
+ function applyLocally(op) {
42
+ if (!snapshot) return;
43
+ const docs = snapshot.collections[op.collection] ??= {};
44
+ if (op.op === "delete") delete docs[op.id];
45
+ else if (op.op === "set") docs[op.id] = structuredClone(op.doc ?? {});
46
+ else docs[op.id] = { ...docs[op.id] ?? {}, ...structuredClone(op.doc ?? {}) };
47
+ }
48
+ async function flush() {
49
+ timer = 0;
50
+ if (inFlight || queue.length === 0) return;
51
+ inFlight = true;
52
+ const batch = queue;
53
+ queue = [];
54
+ try {
55
+ const result = await transport.apply(batch.map(({ op }) => op));
56
+ if (snapshot) {
57
+ snapshot.version = result.version;
58
+ for (const { collection, id, doc } of result.changed) {
59
+ const docs = snapshot.collections[collection] ??= {};
60
+ if (doc === null) delete docs[id];
61
+ else docs[id] = doc;
62
+ }
63
+ for (const { op } of queue) applyLocally(op);
64
+ }
65
+ batch.forEach(({ settle }) => settle());
66
+ notify();
67
+ } catch (error) {
68
+ const reason = error instanceof Error ? error : new Error(String(error));
69
+ batch.forEach(({ settle }) => settle(reason));
70
+ onError(reason);
71
+ await load(true).catch(() => void 0);
72
+ } finally {
73
+ inFlight = false;
74
+ if (queue.length) schedule();
75
+ }
76
+ }
77
+ function schedule() {
78
+ if (!timer) timer = window.setTimeout(() => void flush(), WRITE_BATCH_MS);
79
+ }
80
+ async function write(op) {
81
+ checkName("collection", op.collection);
82
+ checkName("document id", op.id);
83
+ await load();
84
+ return new Promise((resolve, reject) => {
85
+ applyLocally(op);
86
+ queue.push({ op, settle: (error) => error ? reject(error) : resolve() });
87
+ notify();
88
+ schedule();
89
+ });
90
+ }
91
+ return {
92
+ load,
93
+ /** Reads the host's data again (the frame came back into view), unless writes are on their way. */
94
+ refresh() {
95
+ if (snapshot && !inFlight && queue.length === 0) void load(true).catch(() => void 0);
96
+ },
97
+ collection(name) {
98
+ checkName("collection", name);
99
+ const docs = () => snapshot?.collections[name] ?? {};
100
+ return Object.freeze({
101
+ async get(id) {
102
+ await load();
103
+ const doc = docs()[id];
104
+ return doc ? { ...structuredClone(doc), id } : null;
105
+ },
106
+ async list(options = {}) {
107
+ await load();
108
+ let rows = Object.entries(docs()).map(([id, doc]) => ({ ...structuredClone(doc), id }));
109
+ const { where, orderBy, limit } = options;
110
+ if (typeof where === "function") rows = rows.filter(where);
111
+ else if (where) rows = rows.filter((row) => Object.entries(where).every(([key, value]) => row[key] === value));
112
+ if (orderBy) {
113
+ const [field, direction] = Array.isArray(orderBy) ? orderBy : [orderBy, "asc"];
114
+ rows.sort((a, b) => compare(a[field], b[field]) * (direction === "desc" ? -1 : 1));
115
+ }
116
+ return typeof limit === "number" ? rows.slice(0, Math.max(0, limit)) : rows;
117
+ },
118
+ set: (id, doc) => write({ op: "set", collection: name, id, doc }),
119
+ update: (id, patch) => write({ op: "update", collection: name, id, doc: patch }),
120
+ delete: (id) => write({ op: "delete", collection: name, id }),
121
+ /** Calls back whenever the collection may have changed (a write here, or fresh data from the host). */
122
+ subscribe(listener) {
123
+ listeners.add(listener);
124
+ return () => listeners.delete(listener);
125
+ }
126
+ });
127
+ }
128
+ };
129
+ }
130
+ var KV_COLLECTION = "_kv";
131
+ function createDb(transport, onError = () => void 0) {
132
+ const store = createStore(transport, onError);
133
+ const refresh = () => store.refresh();
134
+ window.addEventListener("focus", refresh);
135
+ document.addEventListener("visibilitychange", () => document.visibilityState === "visible" && refresh());
136
+ const db = Object.freeze({
137
+ collection: (name) => store.collection(name)
138
+ });
139
+ const kv = () => store.collection(KV_COLLECTION);
140
+ const storage = Object.freeze({
141
+ async get(key) {
142
+ return (await kv().get(key))?.value;
143
+ },
144
+ set: (key, value) => kv().set(key, { value }),
145
+ delete: (key) => kv().delete(key),
146
+ async list(options = {}) {
147
+ const rows = await kv().list();
148
+ return rows.map((row) => row.id).filter((key) => !options.prefix || key.startsWith(options.prefix));
149
+ }
150
+ });
151
+ return { db, storage };
152
+ }
153
+ export {
154
+ WRITE_BATCH_MS,
155
+ createDb
156
+ };
package/dist/index.d.ts CHANGED
@@ -8,6 +8,8 @@ export declare const kitScript: string;
8
8
  export declare const kitReactScript: string;
9
9
  /** React mode's loader, an IIFE: React from its pinned CDN, the kit's components, then the widget module mounted. */
10
10
  export declare const reactLoaderScript: string;
11
+ /** A downloaded widget's data on its own, an IIFE: `window.dvina` over its seed and localStorage, when no viewer set one. */
12
+ export declare const standaloneScript: string;
11
13
  /** Tailwind's theme over the kit's tokens, as text for a text/tailwindcss style; React artifacts only. */
12
14
  export declare const tailwindCss: string;
13
15
  /** Tailwind v4's browser build (pinned), which compiles that theme and the classes it finds in the page. */