@fireproof/core 0.19.121 → 0.20.0-dev-preview-06

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. package/README.md +3 -2
  2. package/deno/index.d.ts +7 -0
  3. package/deno/index.js +66 -0
  4. package/deno/index.js.map +1 -0
  5. package/deno/metafile-esm.json +1 -0
  6. package/deno.json +2 -3
  7. package/index.cjs +1819 -1051
  8. package/index.cjs.map +1 -1
  9. package/index.d.cts +746 -333
  10. package/index.d.ts +746 -333
  11. package/index.js +1792 -1026
  12. package/index.js.map +1 -1
  13. package/metafile-cjs.json +1 -1
  14. package/metafile-esm.json +1 -1
  15. package/node/index.cjs +16 -293
  16. package/node/index.cjs.map +1 -1
  17. package/node/index.d.cts +4 -40
  18. package/node/index.d.ts +4 -40
  19. package/node/index.js +22 -237
  20. package/node/index.js.map +1 -1
  21. package/node/metafile-cjs.json +1 -1
  22. package/node/metafile-esm.json +1 -1
  23. package/package.json +12 -4
  24. package/react/index.cjs.map +1 -1
  25. package/react/index.js.map +1 -1
  26. package/react/metafile-cjs.json +1 -1
  27. package/react/metafile-esm.json +1 -1
  28. package/tests/blockstore/fp-envelope.test.ts-off +65 -0
  29. package/tests/blockstore/interceptor-gateway.test.ts +122 -0
  30. package/tests/blockstore/keyed-crypto-indexdb-file.test.ts +130 -0
  31. package/tests/blockstore/keyed-crypto.test.ts +73 -118
  32. package/tests/blockstore/loader.test.ts +18 -9
  33. package/tests/blockstore/store.test.ts +40 -31
  34. package/tests/blockstore/transaction.test.ts +14 -13
  35. package/tests/fireproof/all-gateway.test.ts +283 -213
  36. package/tests/fireproof/cars/bafkreidxwt2nhvbl4fnqfw3ctlt6zbrir4kqwmjo5im6rf4q5si27kgo2i.ts +324 -316
  37. package/tests/fireproof/crdt.test.ts +78 -19
  38. package/tests/fireproof/database.test.ts +225 -29
  39. package/tests/fireproof/fireproof.test.ts +92 -73
  40. package/tests/fireproof/hello.test.ts +17 -13
  41. package/tests/fireproof/indexer.test.ts +67 -43
  42. package/tests/fireproof/utils.test.ts +47 -6
  43. package/tests/gateway/file/loader-config.test.ts +307 -0
  44. package/tests/gateway/fp-envelope-serialize.test.ts +256 -0
  45. package/tests/gateway/indexdb/loader-config.test.ts +79 -0
  46. package/tests/helpers.ts +44 -17
  47. package/tests/react/useFireproof.test.tsx +1 -1
  48. package/tests/www/todo.html +24 -3
  49. package/web/index.cjs +102 -116
  50. package/web/index.cjs.map +1 -1
  51. package/web/index.d.cts +15 -29
  52. package/web/index.d.ts +15 -29
  53. package/web/index.js +91 -105
  54. package/web/index.js.map +1 -1
  55. package/web/metafile-cjs.json +1 -1
  56. package/web/metafile-esm.json +1 -1
  57. package/node/chunk-4A4RAVNS.js +0 -17
  58. package/node/chunk-4A4RAVNS.js.map +0 -1
  59. package/node/mem-filesystem-LPPT7QV5.js +0 -40
  60. package/node/mem-filesystem-LPPT7QV5.js.map +0 -1
  61. package/tests/fireproof/config.test.ts +0 -163
  62. /package/tests/blockstore/{fragment-gateway.test.ts → fragment-gateway.test.ts-off} +0 -0
  63. /package/tests/fireproof/{multiple-ledger.test.ts → multiple-database.test.ts} +0 -0
@@ -0,0 +1,256 @@
1
+ import { rt, bs } from "@fireproof/core";
2
+ import { mockSuperThis, simpleCID } from "../helpers.js";
3
+ import { BuildURI, Result } from "@adviser/cement";
4
+ import { toJSON } from "multiformats/link";
5
+
6
+ const FPEnvelopeType = bs.FPEnvelopeType;
7
+
8
+ describe("storage-content", () => {
9
+ const sthis = mockSuperThis();
10
+ it("car", async () => {
11
+ const raw = new Uint8Array([55, 56, 57]);
12
+ const res = await rt.gw.fpDeserialize(sthis, BuildURI.from("http://x.com?store=data&suffix=.car").URI(), Result.Ok(raw));
13
+ expect(res.isOk()).toBeTruthy();
14
+ expect(res.unwrap().type).toEqual(FPEnvelopeType.CAR);
15
+ expect(res.unwrap().payload).toEqual(raw);
16
+ });
17
+
18
+ it("file", async () => {
19
+ const raw = new Uint8Array([55, 56, 57]);
20
+ const res = await rt.gw.fpDeserialize(sthis, BuildURI.from("http://x.com?store=data").URI(), Result.Ok(raw));
21
+ expect(res.isOk()).toBeTruthy();
22
+ expect(res.unwrap().type).toEqual(FPEnvelopeType.FILE);
23
+ expect(res.unwrap().payload).toEqual(raw);
24
+ });
25
+
26
+ it("meta", async () => {
27
+ const ref = [
28
+ {
29
+ cid: "bafyreiaqmtw5jfudn6r6dq7mcmytc2z5z3ggohcj3gco3omjsp3hr73fpy",
30
+ data: "MomRkYXRhoWZkYk1ldGFYU3siY2FycyI6W3siLyI6ImJhZzR5dnFhYmNpcWNod29zeXQ3dTJqMmxtcHpyM2w3aWRlaTU1YzNmNnJ2Z3U3cXRmYXRoMnl2NnZuaWtjeXEifV19Z3BhcmVudHOA",
31
+ parents: [(await simpleCID(sthis)).toString(), (await simpleCID(sthis)).toString()],
32
+ },
33
+ ];
34
+ const raw = sthis.txt.encode(JSON.stringify(ref));
35
+ const res = await rt.gw.fpDeserialize(sthis, BuildURI.from("http://x.com?store=meta").URI(), Result.Ok(raw));
36
+ expect(res.isOk()).toBeTruthy();
37
+ expect(res.unwrap().type).toEqual(FPEnvelopeType.META);
38
+ const dbMetas = res.unwrap().payload as bs.DbMetaEvent[];
39
+ expect(dbMetas.length).toBe(1);
40
+ const dbMeta = dbMetas[0];
41
+ expect(dbMeta.parents.map((i) => i.toString())).toStrictEqual(ref[0].parents);
42
+ expect(dbMeta.eventCid.toString()).toEqual("bafyreiaqmtw5jfudn6r6dq7mcmytc2z5z3ggohcj3gco3omjsp3hr73fpy");
43
+ expect(dbMeta.dbMeta.cars.map((i) => i.toString())).toEqual([
44
+ "bag4yvqabciqchwosyt7u2j2lmpzr3l7idei55c3f6rvgu7qtfath2yv6vnikcyq",
45
+ ]);
46
+ });
47
+
48
+ it("wal", async () => {
49
+ const ref = {
50
+ fileOperations: [
51
+ {
52
+ cid: "bafyreiaqmtw5jfudn6r6dq7mcmytc2z5z3ggohcj3gco3omjsp3hr73fpy",
53
+ public: false,
54
+ },
55
+ ],
56
+ noLoaderOps: [
57
+ {
58
+ cars: [
59
+ {
60
+ "/": "bag4yvqabciqchwosyt7u2j2lmpzr3l7idei55c3f6rvgu7qtfath2yv6vnikcyq",
61
+ },
62
+ ],
63
+ },
64
+ ],
65
+ operations: [
66
+ {
67
+ cars: [{ "/": "bag4yvqabciqchwosyt7u2j2lmpzr3l7idei55c3f6rvgu7qtfath2yv6vnikcyq" }],
68
+ },
69
+ ],
70
+ };
71
+ const raw = sthis.txt.encode(JSON.stringify(ref));
72
+ const res = await rt.gw.fpDeserialize(sthis, BuildURI.from("http://x.com?store=wal").URI(), Result.Ok(raw));
73
+ expect(res.isOk()).toBeTruthy();
74
+ expect(res.unwrap().type).toEqual(FPEnvelopeType.WAL);
75
+ const walstate = res.unwrap().payload as bs.WALState;
76
+ expect(
77
+ walstate.fileOperations.map((i) => ({
78
+ ...i,
79
+ cid: i.cid.toString(),
80
+ })),
81
+ ).toEqual(ref.fileOperations);
82
+ expect(
83
+ walstate.noLoaderOps.map((i) => ({
84
+ cars: i.cars.map((i) => toJSON(i)),
85
+ })),
86
+ ).toEqual(ref.noLoaderOps);
87
+ expect(
88
+ walstate.operations.map((i) => ({
89
+ cars: i.cars.map((i) => toJSON(i)),
90
+ })),
91
+ ).toEqual(ref.operations);
92
+ });
93
+ });
94
+
95
+ describe("de-serialize", () => {
96
+ const sthis = mockSuperThis();
97
+ it("car", async () => {
98
+ const msg = {
99
+ type: FPEnvelopeType.CAR,
100
+ payload: new Uint8Array([55, 56, 57]),
101
+ } satisfies bs.FPEnvelopeCar;
102
+ const res = await rt.gw.fpSerialize(sthis, msg);
103
+ expect(res.Ok()).toEqual(msg.payload);
104
+ });
105
+
106
+ it("file", async () => {
107
+ const msg = {
108
+ type: FPEnvelopeType.FILE,
109
+ payload: new Uint8Array([55, 56, 57]),
110
+ } satisfies bs.FPEnvelopeFile;
111
+ const res = await rt.gw.fpSerialize(sthis, msg);
112
+ expect(res.Ok()).toEqual(msg.payload);
113
+ });
114
+
115
+ it("meta", async () => {
116
+ const msg = {
117
+ type: FPEnvelopeType.META,
118
+ payload: [
119
+ await bs.createDbMetaEvent(
120
+ sthis,
121
+ {
122
+ cars: [await simpleCID(sthis)],
123
+ },
124
+ [await simpleCID(sthis), await simpleCID(sthis)],
125
+ ),
126
+ ],
127
+ } satisfies bs.FPEnvelopeMeta;
128
+ const ser = await rt.gw.fpSerialize(sthis, msg);
129
+ const res = await rt.gw.fpDeserialize(sthis, BuildURI.from("http://x.com?store=meta").URI(), ser);
130
+ const dbMetas = res.unwrap().payload as bs.DbMetaEvent[];
131
+ expect(dbMetas.length).toBe(1);
132
+ const dbMeta = dbMetas[0];
133
+ expect(dbMeta.parents).toEqual(msg.payload[0].parents);
134
+ expect(dbMeta.dbMeta).toEqual(msg.payload[0].dbMeta);
135
+ expect(dbMeta.eventCid).toEqual(msg.payload[0].eventCid);
136
+ });
137
+
138
+ it("wal", async () => {
139
+ const msg = {
140
+ type: FPEnvelopeType.WAL,
141
+ payload: {
142
+ fileOperations: [
143
+ {
144
+ cid: await simpleCID(sthis),
145
+ public: false,
146
+ },
147
+ ],
148
+ noLoaderOps: [
149
+ {
150
+ cars: [await simpleCID(sthis)],
151
+ },
152
+ ],
153
+ operations: [
154
+ {
155
+ cars: [await simpleCID(sthis)],
156
+ },
157
+ ],
158
+ },
159
+ } satisfies bs.FPEnvelopeWAL;
160
+ const ser = await rt.gw.fpSerialize(sthis, msg);
161
+ const res = await rt.gw.fpDeserialize(sthis, BuildURI.from("http://x.com?store=wal").URI(), ser);
162
+ expect(res.isOk()).toBeTruthy();
163
+ expect(res.unwrap().type).toEqual("wal");
164
+ const walstate = res.unwrap().payload as bs.WALState;
165
+ expect(walstate.fileOperations).toEqual(msg.payload.fileOperations);
166
+ expect(walstate.noLoaderOps).toEqual(msg.payload.noLoaderOps);
167
+ expect(walstate.operations).toEqual(msg.payload.operations);
168
+ });
169
+
170
+ it("coerce into fpDeserialize Result", async () => {
171
+ const raw = new Uint8Array([55, 56, 57]);
172
+ const res = await rt.gw.fpDeserialize(sthis, BuildURI.from("http://x.com?store=data&suffix=.car").URI(), Result.Ok(raw));
173
+ expect(res.isOk()).toBeTruthy();
174
+ expect(res.unwrap().type).toEqual(FPEnvelopeType.CAR);
175
+ expect(res.unwrap().payload).toEqual(raw);
176
+ });
177
+
178
+ it("coerce into fpDeserialize Promise", async () => {
179
+ const raw = new Uint8Array([55, 56, 57]);
180
+ const res = await rt.gw.fpDeserialize(sthis, BuildURI.from("http://x.com?store=data&suffix=.car").URI(), Promise.resolve(raw));
181
+ expect(res.isOk()).toBeTruthy();
182
+ expect(res.unwrap().type).toEqual(FPEnvelopeType.CAR);
183
+ expect(res.unwrap().payload).toEqual(raw);
184
+ });
185
+
186
+ it("coerce into fpDeserialize Promise Result", async () => {
187
+ const raw = new Uint8Array([55, 56, 57]);
188
+ const res = await rt.gw.fpDeserialize(
189
+ sthis,
190
+ BuildURI.from("http://x.com?store=data&suffix=.car").URI(),
191
+ Promise.resolve(Result.Ok(raw)),
192
+ );
193
+ expect(res.isOk()).toBeTruthy();
194
+ expect(res.unwrap().type).toEqual(FPEnvelopeType.CAR);
195
+ expect(res.unwrap().payload).toEqual(raw);
196
+ });
197
+
198
+ it("coerce into fpDeserialize Promise Result.Err", async () => {
199
+ const raw = Promise.resolve(Result.Err<Uint8Array>("error"));
200
+ const res = await rt.gw.fpDeserialize(sthis, BuildURI.from("http://x.com?store=data&suffix=.car").URI(), raw);
201
+ expect(res.isErr()).toBeTruthy();
202
+ expect(res.unwrap_err().message).toEqual("error");
203
+ });
204
+
205
+ it("coerce into fpDeserialize Promise.reject", async () => {
206
+ const raw = Promise.reject(new Error("error"));
207
+ const res = await rt.gw.fpDeserialize(sthis, BuildURI.from("http://x.com?store=data&suffix=.car").URI(), raw);
208
+ expect(res.isErr()).toBeTruthy();
209
+ expect(res.unwrap_err().message).toEqual("error");
210
+ });
211
+
212
+ it("coerce into fpDeserialize Result.Err", async () => {
213
+ const raw = Result.Err<Uint8Array>("error");
214
+ const res = await rt.gw.fpDeserialize(sthis, BuildURI.from("http://x.com?store=data&suffix=.car").URI(), raw);
215
+ expect(res.isErr()).toBeTruthy();
216
+ expect(res.unwrap_err().message).toEqual("error");
217
+ });
218
+
219
+ it("attach Key to Meta", async () => {
220
+ const msg = {
221
+ type: FPEnvelopeType.META,
222
+ payload: [
223
+ await bs.createDbMetaEvent(
224
+ sthis,
225
+ {
226
+ cars: [await simpleCID(sthis)],
227
+ },
228
+ [await simpleCID(sthis), await simpleCID(sthis)],
229
+ ),
230
+ ],
231
+ } satisfies bs.FPEnvelopeMeta;
232
+ const ser = await rt.gw.fpSerialize(sthis, msg, {
233
+ meta: async (sthis, payload) => {
234
+ return Result.Ok(sthis.txt.encode(JSON.stringify(payload.map((i) => ({ ...i, key: "key" })))));
235
+ },
236
+ });
237
+ let key = "";
238
+ const res = await rt.gw.fpDeserialize(sthis, BuildURI.from("http://x.com?store=meta").URI(), ser, {
239
+ meta: async (sthis, payload) => {
240
+ const json = JSON.parse(sthis.txt.decode(payload));
241
+ key = json[0].key;
242
+ return Result.Ok(
243
+ json.map((i: { key?: string }) => {
244
+ delete i.key;
245
+ return i;
246
+ }) as rt.gw.SerializedMeta[],
247
+ );
248
+ },
249
+ });
250
+ expect(res.isOk()).toBeTruthy();
251
+ const meta = res.unwrap() as bs.FPEnvelopeMeta;
252
+ expect(meta.type).toEqual("meta");
253
+ expect(Object.keys(meta.payload).includes("key")).toBeFalsy();
254
+ expect(key).toEqual("key");
255
+ });
256
+ });
@@ -0,0 +1,79 @@
1
+ import { fireproof } from "@fireproof/core";
2
+ import { mockSuperThis } from "../../helpers.js";
3
+
4
+ describe("fireproof config indexdb", () => {
5
+ const _my_app = "my-app";
6
+ function my_app() {
7
+ return _my_app;
8
+ }
9
+ const sthis = mockSuperThis();
10
+ beforeAll(async () => {
11
+ await sthis.start();
12
+ });
13
+
14
+ it("indexdb-loader", async () => {
15
+ const db = fireproof(my_app());
16
+ await db.put({ name: "my-app" });
17
+ expect(db.name).toBe(my_app());
18
+
19
+ const fileStore = await db.crdt.blockstore.loader.fileStore();
20
+ expect(fileStore?.url().asObj()).toEqual({
21
+ pathname: "fp",
22
+ protocol: "indexdb:",
23
+ searchParams: {
24
+ name: "my-app",
25
+ store: "data",
26
+ runtime: "browser",
27
+ storekey: "@my-app-data@",
28
+ urlGen: "default",
29
+ version: "v0.19-indexdb",
30
+ },
31
+ style: "path",
32
+ });
33
+
34
+ const dataStore = await db.crdt.blockstore.loader.carStore();
35
+ expect(dataStore?.url().asObj()).toEqual({
36
+ pathname: "fp",
37
+ protocol: "indexdb:",
38
+ searchParams: {
39
+ name: "my-app",
40
+ store: "data",
41
+ runtime: "browser",
42
+ storekey: "@my-app-data@",
43
+ suffix: ".car",
44
+ urlGen: "default",
45
+ version: "v0.19-indexdb",
46
+ },
47
+ style: "path",
48
+ });
49
+ const metaStore = await db.crdt.blockstore.loader.metaStore();
50
+ expect(metaStore?.url().asObj()).toEqual({
51
+ pathname: "fp",
52
+ protocol: "indexdb:",
53
+ searchParams: {
54
+ name: "my-app",
55
+ store: "meta",
56
+ runtime: "browser",
57
+ storekey: "@my-app-meta@",
58
+ urlGen: "default",
59
+ version: "v0.19-indexdb",
60
+ },
61
+ style: "path",
62
+ });
63
+ const WALStore = await db.crdt.blockstore.loader.WALStore();
64
+ expect(WALStore?.url().asObj()).toEqual({
65
+ pathname: "fp",
66
+ protocol: "indexdb:",
67
+ searchParams: {
68
+ name: "my-app",
69
+ store: "wal",
70
+ runtime: "browser",
71
+ storekey: "@my-app-wal@",
72
+ urlGen: "default",
73
+ version: "v0.19-indexdb",
74
+ },
75
+ style: "path",
76
+ });
77
+ await db.close();
78
+ });
79
+ });
package/tests/helpers.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { runtimeFn, toCryptoRuntime, URI } from "@adviser/cement";
2
- import { dataDir, rt, SuperThis } from "@fireproof/core";
3
-
4
- export { dataDir };
1
+ import { BuildURI, MockLogger, runtimeFn, toCryptoRuntime, URI, utils, LogCollector } from "@adviser/cement";
2
+ import { ensureSuperThis, rt, SuperThis, SuperThisOpts, bs, PARAM } from "@fireproof/core";
3
+ import { CID } from "multiformats";
4
+ import { sha256 } from "multiformats/hashes/sha2";
5
+ import * as json from "multiformats/codecs/json";
5
6
 
6
7
  export function sleep(ms: number) {
7
8
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -37,16 +38,42 @@ export function storageURL(sthis: SuperThis): URI {
37
38
  return merged;
38
39
  }
39
40
 
40
- // export type MockSuperThis = SuperThis & { logCollector: LogCollector };
41
- // // eslint-disable-next-line @typescript-eslint/no-unused-vars
42
- // export function mockSuperThis(sthis?: Partial<SuperThisOpts>): MockSuperThis {
43
- // const mockLog = MockLogger();
44
- // const ethis = ensureSuperThis({
45
- // logger: mockLog.logger,
46
- // ctx: {
47
- // logCollector: mockLog.logCollector,
48
- // },
49
- // }) as MockSuperThis;
50
- // ethis.logCollector = mockLog.logCollector;
51
- // return ethis;
52
- // }
41
+ export type MockSuperThis = SuperThis & { ctx: { readonly logCollector: LogCollector } };
42
+ export function mockSuperThis(sthis?: Partial<SuperThisOpts>): MockSuperThis {
43
+ const mockLog = MockLogger({
44
+ pass: new utils.ConsoleWriterStreamDefaultWriter(new utils.ConsoleWriterStream()),
45
+ });
46
+ return ensureSuperThis({
47
+ ...sthis,
48
+ logger: mockLog.logger,
49
+ ctx: {
50
+ logCollector: mockLog.logCollector,
51
+ },
52
+ }) as MockSuperThis;
53
+ }
54
+
55
+ export function noopUrl(name?: string): URI {
56
+ const burl = BuildURI.from("memory://noop");
57
+ burl.setParam(PARAM.NAME, name || "test");
58
+ return burl.URI();
59
+ }
60
+
61
+ export function simpleBlockOpts(sthis: SuperThis, name?: string) {
62
+ const url = noopUrl(name);
63
+ return {
64
+ keyBag: rt.kb.defaultKeyBagOpts(sthis),
65
+ storeRuntime: bs.toStoreRuntime(sthis),
66
+ storeUrls: {
67
+ file: url,
68
+ wal: url,
69
+ meta: url,
70
+ data: url,
71
+ },
72
+ };
73
+ }
74
+
75
+ export async function simpleCID(sthis: SuperThis) {
76
+ const bytes = json.encode({ hello: sthis.nextId().str });
77
+ const hash = await sha256.digest(bytes);
78
+ return CID.create(1, json.code, hash);
79
+ }
@@ -13,7 +13,7 @@ describe("HOOK: useFireproof", () => {
13
13
  const { database, useLiveQuery, useDocument } = useFireproof("dbname");
14
14
  expect(typeof useLiveQuery).toBe("function");
15
15
  expect(typeof useDocument).toBe("function");
16
- expect(database?.constructor.name).toBe("Database");
16
+ expect(database?.constructor.name).toMatch(/^Database/);
17
17
  });
18
18
  });
19
19
  });
@@ -75,8 +75,9 @@
75
75
 
76
76
  let compactor = "🚗";
77
77
  function drawInfo() {
78
- document.querySelector("#carLog").innerText =
79
- ` ⏰ ${db._crdt.clock.head.length} ${compactor} ${cx.loader.carLog.length} 📩 ${1}`;
78
+ document.querySelector("#carLog").innerText = ` ⏰ ${db._crdt.clock.head.length} ${compactor} ${
79
+ cx.loader.carLog.length
80
+ } 📩 ${1}`;
80
81
  }
81
82
  const doRedraw = async () => {
82
83
  drawInfo();
@@ -164,7 +165,7 @@
164
165
  const timeout =
165
166
  10 +
166
167
  Math.pow(db._crdt.clock.head.length + cx.loader.taskManager.queue.length, 2) *
167
- (Math.floor(Math.random() * 2000) + 2000);
168
+ (Math.floor(Math.random() * 1000) + 1000);
168
169
  // console.log('go worker', timeout/ 1000)
169
170
  worker = setTimeout(async () => {
170
171
  await Promise.all(
@@ -195,6 +196,25 @@
195
196
  };
196
197
  window.toggleWorker = toggleWorker;
197
198
 
199
+ async function writeStorm(e) {
200
+ e.preventDefault();
201
+ compactor = "🐦‍🔥";
202
+ drawInfo();
203
+
204
+ const dcs = await db.allDocs();
205
+ const lastRow = dcs.rows[dcs.rows.length - 1];
206
+ const theDoc = lastRow.value;
207
+ for (let i = 0; i < 50; i++) {
208
+ theDoc.clicks = (theDoc.clicks || 0) + 1;
209
+ theDoc.completed = !theDoc.completed;
210
+ await db.put(theDoc);
211
+ }
212
+
213
+ drawInfo();
214
+ compactor = "🚗";
215
+ }
216
+ window.writeStorm = writeStorm;
217
+
198
218
  async function doCompact(e) {
199
219
  e.preventDefault();
200
220
  compactor = "🚕";
@@ -221,6 +241,7 @@
221
241
 
222
242
  <p>This version of the Fireprof test app uses PartyKit as the storage backend. Refresh is automatic and live.</p>
223
243
 
244
+ <button id="rocket" onclick="writeStorm(event)">🚀</button>
224
245
  <button id="robot" onclick="toggleWorker(event)">🤖</button>
225
246
  <span id="carLog" onclick="doCompact(event)"></span>
226
247
  <span id="cxInfo"></span>