@mostra/react 0.0.0-stage → 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/LICENSE +202 -0
- package/README.md +16 -2
- package/dist/form.d.ts +20 -0
- package/dist/form.d.ts.map +1 -0
- package/dist/form.js +259 -0
- package/dist/form.js.map +1 -0
- package/dist/hooks-Dh_l8KqJ.js +2737 -0
- package/dist/hooks-Dh_l8KqJ.js.map +1 -0
- package/dist/index.d.ts +412 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +199 -0
- package/dist/index.js.map +1 -0
- package/dist/mount-Bpg0M5ln.js +516 -0
- package/dist/mount-Bpg0M5ln.js.map +1 -0
- package/dist/router.d.ts +28 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +52 -0
- package/dist/router.js.map +1 -0
- package/package.json +117 -3
- package/src/form.ts +326 -0
- package/src/hooks.ts +530 -0
- package/src/index.ts +75 -0
- package/src/mount.ts +78 -0
- package/src/router.ts +84 -0
- package/src/runtime/boot.ts +306 -0
- package/src/runtime/bridge-limits.ts +30 -0
- package/src/runtime/errors.ts +53 -0
- package/src/runtime/journal.ts +219 -0
- package/src/runtime/server-client.ts +745 -0
- package/src/runtime/store.ts +1641 -0
- package/src/runtime/transport.ts +342 -0
- package/src/runtime/verify-probe.ts +67 -0
- package/src/server-hooks.ts +291 -0
- package/src/server-reference.ts +54 -0
- package/src/shell-bridge.ts +212 -0
- package/src/storage.ts +165 -0
package/src/storage.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `window.storage`, compatible with Claude artifacts' persistent storage (D53, D112): agents that
|
|
5
|
+
* learned `await window.storage.get(key)` keep working, and their values become Mostra fields.
|
|
6
|
+
* Personal keys are the viewer's draft fields `storage.<key>`, carried over between versions and
|
|
7
|
+
* delivered with the submission; `shared: true` keys are artifact state, every viewer's (D63).
|
|
8
|
+
*
|
|
9
|
+
* Differences from Claude's, by design (D112): a value is any JSON value up to 1 MiB (Mostra's
|
|
10
|
+
* field limit; a larger one rejects with `MOSTRA_FIELD_TOO_LARGE`, never truncated), a missing key
|
|
11
|
+
* resolves `null`, and two writers of one key end with the last write (a write that keeps meeting
|
|
12
|
+
* another writer's change is refused with `MOSTRA_FIELD_CONFLICT`, never reported saved).
|
|
13
|
+
*/
|
|
14
|
+
import { isFieldId } from "@mostra/protocol";
|
|
15
|
+
import { MostraError } from "./runtime/errors.ts";
|
|
16
|
+
import { fieldKey, type Store } from "./runtime/store.ts";
|
|
17
|
+
|
|
18
|
+
/** What `get` and `set` resolve with. */
|
|
19
|
+
export interface StorageEntry {
|
|
20
|
+
/** The key. */
|
|
21
|
+
readonly key: string;
|
|
22
|
+
/** Its value: any JSON value. */
|
|
23
|
+
readonly value: unknown;
|
|
24
|
+
/** Whether it is shared by every viewer (artifact state) or the viewer's own. */
|
|
25
|
+
readonly shared: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The storage API of Claude artifacts, backed by Mostra fields (D53). */
|
|
29
|
+
export interface ClaudeCompatibleStorage {
|
|
30
|
+
/** The value of `key`, or `null` when it has none. */
|
|
31
|
+
get(key: string, shared?: boolean): Promise<StorageEntry | null>;
|
|
32
|
+
/** Stores `value` under `key`; resolves once Mostra saved it. */
|
|
33
|
+
set(key: string, value: unknown, shared?: boolean): Promise<StorageEntry>;
|
|
34
|
+
/** Removes `key`; resolves once Mostra saved that. */
|
|
35
|
+
delete(
|
|
36
|
+
key: string,
|
|
37
|
+
shared?: boolean,
|
|
38
|
+
): Promise<{ readonly key: string; readonly deleted: true; readonly shared: boolean }>;
|
|
39
|
+
/** The keys stored, those starting with `prefix` when it is given. */
|
|
40
|
+
list(
|
|
41
|
+
prefix?: string,
|
|
42
|
+
shared?: boolean,
|
|
43
|
+
): Promise<{ readonly keys: string[]; readonly prefix?: string; readonly shared: boolean }>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const PREFIX = "storage.";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* How many times a write over another writer's change is tried (last write wins, D112) before it
|
|
50
|
+
* is refused with `MOSTRA_FIELD_CONFLICT`.
|
|
51
|
+
*/
|
|
52
|
+
const ATTEMPTS = 6;
|
|
53
|
+
|
|
54
|
+
function fieldIdOf(key: unknown): string {
|
|
55
|
+
if (typeof key !== "string" || !isFieldId(`${PREFIX}${key}`)) {
|
|
56
|
+
throw new MostraError(
|
|
57
|
+
"MOSTRA_FIELD_ID_INVALID",
|
|
58
|
+
`${JSON.stringify(key)} can't be a storage key: use 1 to 248 characters without control characters or empty dot-separated parts.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return `${PREFIX}${key}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Creates the `window.storage` of a page's store. */
|
|
65
|
+
export function createStorage(store: Store): ClaudeCompatibleStorage {
|
|
66
|
+
const write = async (key: string, value: unknown, shared: boolean): Promise<void> => {
|
|
67
|
+
const id = fieldIdOf(key);
|
|
68
|
+
const scope = shared ? "artifact" : "viewer";
|
|
69
|
+
await store.whenReady();
|
|
70
|
+
if (value !== undefined) {
|
|
71
|
+
const text = JSON.stringify(value);
|
|
72
|
+
if (text === undefined) {
|
|
73
|
+
throw new MostraError("MOSTRA_HOST_INVALID_PARAMS", `The value of ${key} isn't JSON.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
store.set(scope, id, value, {
|
|
77
|
+
immediate: true,
|
|
78
|
+
...(value === undefined ? { delete: true } : {}),
|
|
79
|
+
});
|
|
80
|
+
const k = fieldKey(scope, id);
|
|
81
|
+
// Settled: saved, refused or in conflict. Offline, the write waits in the journal and the
|
|
82
|
+
// promise with it: it resolves when Mostra has the value, never before (GPT review finding 10).
|
|
83
|
+
const settled = (): Promise<void> =>
|
|
84
|
+
new Promise((resolve) => {
|
|
85
|
+
const done = (): boolean =>
|
|
86
|
+
store.refusal(k) !== undefined ||
|
|
87
|
+
store.conflict(k) !== undefined ||
|
|
88
|
+
store.save(k) === "saved" ||
|
|
89
|
+
store.save(k) === "readonly";
|
|
90
|
+
if (done()) {
|
|
91
|
+
resolve();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const stop = store.subscribe(() => {
|
|
95
|
+
if (!done()) return;
|
|
96
|
+
stop();
|
|
97
|
+
resolve();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
for (let attempt = 0; attempt < ATTEMPTS; attempt += 1) {
|
|
101
|
+
await store.flush();
|
|
102
|
+
await settled();
|
|
103
|
+
const refused = store.refusal(k);
|
|
104
|
+
if (refused !== undefined) {
|
|
105
|
+
store.takeTheirs(k);
|
|
106
|
+
throw new MostraError(refused.code, refused.message);
|
|
107
|
+
}
|
|
108
|
+
if (!store.conflict(k)) return;
|
|
109
|
+
if (attempt === ATTEMPTS - 1) break;
|
|
110
|
+
// Another writer got in between: written again over theirs, after a short, growing pause.
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, 20 * 2 ** attempt * (1 + Math.random())));
|
|
112
|
+
store.keepMine(k);
|
|
113
|
+
}
|
|
114
|
+
// Another writer kept changing it: the write is refused, never reported saved, and the field
|
|
115
|
+
// keeps their value (Claude review finding 9).
|
|
116
|
+
store.takeTheirs(k);
|
|
117
|
+
throw new MostraError(
|
|
118
|
+
"MOSTRA_FIELD_CONFLICT",
|
|
119
|
+
`${key} kept changing under another writer, so this write wasn't saved.`,
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
async get(key, shared = false) {
|
|
124
|
+
const id = fieldIdOf(key);
|
|
125
|
+
await store.whenReady();
|
|
126
|
+
const value = store.value(fieldKey(shared ? "artifact" : "viewer", id), undefined);
|
|
127
|
+
return value === undefined ? null : { key, value, shared };
|
|
128
|
+
},
|
|
129
|
+
async set(key, value, shared = false) {
|
|
130
|
+
await write(key, value, shared);
|
|
131
|
+
return { key, value, shared };
|
|
132
|
+
},
|
|
133
|
+
async delete(key, shared = false) {
|
|
134
|
+
await write(key, undefined, shared);
|
|
135
|
+
return { key, deleted: true as const, shared };
|
|
136
|
+
},
|
|
137
|
+
async list(prefix, shared = false) {
|
|
138
|
+
await store.whenReady();
|
|
139
|
+
const keys = store
|
|
140
|
+
.ids(shared ? "artifact" : "viewer")
|
|
141
|
+
.filter((id) => id.startsWith(PREFIX))
|
|
142
|
+
.map((id) => id.slice(PREFIX.length))
|
|
143
|
+
.filter((key) => prefix === undefined || key.startsWith(prefix));
|
|
144
|
+
return { keys, ...(prefix !== undefined ? { prefix } : {}), shared };
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
declare global {
|
|
150
|
+
interface Window {
|
|
151
|
+
/** Claude-compatible persistent storage, backed by Mostra fields (D53). */
|
|
152
|
+
storage: ClaudeCompatibleStorage;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Installs `window.storage` for the page's store. */
|
|
157
|
+
export function installStorage(store: Store): void {
|
|
158
|
+
if (typeof window === "undefined") return;
|
|
159
|
+
Object.defineProperty(window, "storage", {
|
|
160
|
+
value: createStorage(store),
|
|
161
|
+
configurable: true,
|
|
162
|
+
enumerable: false,
|
|
163
|
+
writable: true,
|
|
164
|
+
});
|
|
165
|
+
}
|