@party-stack/web-runtime 0.4.2 → 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/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # @party-stack/web-runtime
2
+
3
+ The main export constructs the platform runtime as an Effect Layer:
4
+
5
+ ```ts
6
+ import { Runtime } from "@party-stack/runtime";
7
+ import * as WebRuntime from "@party-stack/web-runtime";
8
+ import { Effect } from "effect";
9
+
10
+ const program = Effect.gen(function* () {
11
+ const runtime = yield* Runtime;
12
+ yield* runtime.bytes.write("example", new Blob(["hello"]));
13
+ // Run the application's scoped services here.
14
+ });
15
+
16
+ await Effect.runPromise(program.pipe(Effect.provide(WebRuntime.layer({ owner: "user", namespace: "app" }))));
17
+ ```
18
+
19
+ Defining the Layer opens nothing. Building it acquires storage, connectivity and
20
+ platform resources in its Scope. Completion, failure or interruption closes
21
+ the database, removes listeners, closes channels and releases native locks.
22
+ No separate platform `cleanup()` call is needed. The implementation also works in
23
+ workers, where `window` and `document` are unavailable.
24
+
25
+ Persistent storage uses IndexedDB and OPFS. A persistent replica acquires one native
26
+ Web Lock for its entire host Scope; another owner waits until that Scope closes or
27
+ the browser context dies. SharedWorker clients use the standard ontology protocol
28
+ and memory replicas. BroadcastChannel carries storage invalidations for local
29
+ private-state projections; it is not an RPC transport or leader-election service.
30
+ Missing Web Locks fail explicitly for persistent storage.
31
+
32
+ `storage: "memory"` selects isolated in-process storage. This is
33
+ an explicit fallback choice, with memory lifetime; it does not recover pending
34
+ work from an inaccessible persistent store. Worker support and storage access
35
+ must be tested separately. Private browsing may provide temporary storage with
36
+ different quotas. Browser-local encryption key persistence requires an explicit
37
+ `persistEncryptionKeys: true` choice.
38
+
39
+ ```ts
40
+ WebRuntime.layer({ owner: "user", namespace: "app", persistEncryptionKeys: true });
41
+ ```
42
+
43
+ This explicitly permits a non-extractable AES-GCM CryptoKey to survive reloads in
44
+ IndexedDB, in a separate key database. Same-origin code can still use the key to
45
+ decrypt; it is not a vault lock or isolation from XSS. Clearing site data loses
46
+ these keys. The flag enables persistent local credentials but does not opt ordinary
47
+ encrypted properties into caching. `ontology.decrypt(value, { cache: true })` is a
48
+ separate per-call choice; the default bypasses persistent cached plaintext copies.
49
+ Cached copies are themselves encrypted with the device key. This runtime must be
50
+ partitioned by authenticated owner and ontology namespace as usual; never reuse a
51
+ private runtime for unrelated users. See [encrypted values](../ontology/encrypted-values.md).
52
+
53
+ `createWebRuntime(owner, namespace, options?)` returns the same scoped Effect as
54
+ `make({ owner, namespace, ...options })`. It has no Promise constructor or manual
55
+ cleanup method. Installations accept this provider directly; application entry
56
+ points can supply `layer` to an Effect program or an outer ManagedRuntime.
57
+
58
+ Browser tests cover native IndexedDB transactions/notifications, host-lock release
59
+ on tab termination, and a SharedWorker owner with per-tab memory replicas and
60
+ TanStack collections. Private browsing/storage access still requires platform testing.
@@ -1,16 +1,9 @@
1
- import { NetworkConnectivity } from "@party-stack/runtime";
1
+ import { Effect, Stream } from "effect";
2
2
  //#region src/NavigatorNetworkConnectivity.d.ts
3
- export declare class NavigatorNetworkConnectivity implements NetworkConnectivity {
4
- private readonly listeners;
5
- private lifetime;
6
- private connected;
7
- private closePromise;
8
- private constructor();
9
- static create(): NavigatorNetworkConnectivity;
10
- get isConnected(): boolean;
11
- subscribe(listener: (isConnected: boolean) => void): () => void;
12
- close(): Promise<void>;
13
- private setConnected;
14
- }
3
+ /** Native listeners and the worker event fallback belong to the caller's platform scope. */
4
+ export declare const makeNavigatorNetworkConnectivity: () => Effect.Effect<{
5
+ online: Effect.Effect<boolean, never, never>;
6
+ changes: Stream.Stream<boolean, never, never>;
7
+ }, never, import("effect/Scope").Scope>;
15
8
  //#endregion
16
9
  //# sourceMappingURL=NavigatorNetworkConnectivity.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NavigatorNetworkConnectivity.d.ts","names":[],"sources":["../src/NavigatorNetworkConnectivity.ts"],"mappings":";;qBAKa,wCAAwC;mBAChC;UACT;UACA;UACA;UAED;SAIA,UAAU;MAqCb;EAIJ,UAAU,WAAW;EAOrB,SAAS;UAOD"}
1
+ {"version":3,"file":"NavigatorNetworkConnectivity.d.ts","names":[],"sources":["../src/NavigatorNetworkConnectivity.ts"],"mappings":";;;qBAMa,wCAAgC,OAAA;;;iCA+C3C"}
@@ -1,64 +1,43 @@
1
- import { once, race, run, sleep } from "effection";
1
+ import { makeNetworkConnectivity } from "@party-stack/runtime";
2
+ import { Effect, PubSub, Stream } from "effect";
2
3
  //#region src/NavigatorNetworkConnectivity.ts
3
- const ONLINE_STABILIZATION_MS = 1e3;
4
- var NavigatorNetworkConnectivity = class NavigatorNetworkConnectivity {
5
- listeners = /* @__PURE__ */ new Set();
6
- lifetime;
7
- connected;
8
- closePromise;
9
- constructor(initiallyConnected) {
10
- this.connected = initiallyConnected;
11
- }
12
- static create() {
13
- const connectivity = new NavigatorNetworkConnectivity(navigator.onLine);
14
- connectivity.lifetime = run(function* () {
15
- while (true) {
16
- if (!(yield* race([(function* () {
17
- yield* once(window, "online");
18
- return true;
19
- })(), (function* () {
20
- yield* once(window, "offline");
21
- return false;
22
- })()]))) {
23
- connectivity.setConnected(false);
24
- continue;
25
- }
26
- if (connectivity.isConnected) continue;
27
- const stabilized = yield* race([(function* () {
28
- yield* sleep(ONLINE_STABILIZATION_MS);
29
- return navigator.onLine;
30
- })(), (function* () {
31
- yield* once(window, "offline");
32
- return false;
33
- })()]);
34
- connectivity.setConnected(stabilized);
35
- }
36
- });
37
- connectivity.lifetime.catch(() => void 0);
38
- return connectivity;
39
- }
40
- get isConnected() {
41
- return this.connected;
42
- }
43
- subscribe(listener) {
44
- this.listeners.add(listener);
45
- return () => {
46
- this.listeners.delete(listener);
47
- };
48
- }
49
- close() {
50
- this.closePromise ??= Promise.resolve(this.lifetime.halt()).finally(() => {
51
- this.listeners.clear();
52
- });
53
- return this.closePromise;
54
- }
55
- setConnected(isConnected) {
56
- if (isConnected === this.connected) return;
57
- this.connected = isConnected;
58
- for (const listener of this.listeners) listener(isConnected);
59
- }
60
- };
4
+ const WORKER_CONNECTIVITY_SAMPLE_MS = 1e3;
5
+ /** Native listeners and the worker event fallback belong to the caller's platform scope. */
6
+ const makeNavigatorNetworkConnectivity = Effect.fnUntraced(function* () {
7
+ const state = yield* makeNetworkConnectivity(navigator.onLine !== false);
8
+ const target = typeof window === "undefined" ? globalThis : window;
9
+ const events = yield* Effect.acquireRelease(PubSub.sliding({
10
+ capacity: 1,
11
+ replay: 1
12
+ }), PubSub.shutdown);
13
+ let observed = navigator.onLine !== false;
14
+ const changed = () => {
15
+ observed = navigator.onLine !== false;
16
+ PubSub.publishUnsafe(events, void 0);
17
+ };
18
+ const sample = () => {
19
+ if (observed !== (navigator.onLine !== false)) changed();
20
+ };
21
+ yield* Effect.acquireRelease(Effect.sync(() => {
22
+ target.addEventListener("online", changed);
23
+ target.addEventListener("offline", changed);
24
+ }), () => Effect.sync(() => {
25
+ target.removeEventListener("online", changed);
26
+ target.removeEventListener("offline", changed);
27
+ }));
28
+ const changes = Stream.fromPubSub(events).pipe(Stream.mapEffect(() => state.set(navigator.onLine !== false)));
29
+ yield* Stream.runDrain(changes).pipe(Effect.forkScoped({ startImmediately: true }));
30
+ if (typeof window === "undefined") yield* Effect.forever(Effect.sleep(WORKER_CONNECTIVITY_SAMPLE_MS).pipe(Effect.andThen(Effect.sync(sample)))).pipe(Effect.forkScoped);
31
+ return {
32
+ online: Effect.suspend(() => {
33
+ sample();
34
+ const online = navigator.onLine !== false;
35
+ return state.set(online).pipe(Effect.as(online));
36
+ }),
37
+ changes: state.changes
38
+ };
39
+ });
61
40
  //#endregion
62
- export { NavigatorNetworkConnectivity };
41
+ export { makeNavigatorNetworkConnectivity };
63
42
 
64
43
  //# sourceMappingURL=NavigatorNetworkConnectivity.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"NavigatorNetworkConnectivity.js","names":[],"sources":["../src/NavigatorNetworkConnectivity.ts"],"sourcesContent":["import { once, race, run, sleep, type Task } from \"effection\";\nimport type { NetworkConnectivity } from \"@party-stack/runtime\";\n\nconst ONLINE_STABILIZATION_MS = 1_000;\n\nexport class NavigatorNetworkConnectivity implements NetworkConnectivity {\n private readonly listeners = new Set<(isConnected: boolean) => void>();\n private lifetime!: Task<void>;\n private connected: boolean;\n private closePromise: Promise<void> | undefined;\n\n private constructor(initiallyConnected: boolean) {\n this.connected = initiallyConnected;\n }\n\n static create(): NavigatorNetworkConnectivity {\n const connectivity = new NavigatorNetworkConnectivity(navigator.onLine);\n connectivity.lifetime = run(function* () {\n while (true) {\n const online = yield* race([\n (function* () {\n yield* once(window, \"online\");\n return true;\n })(),\n (function* () {\n yield* once(window, \"offline\");\n return false;\n })(),\n ]);\n if (!online) {\n connectivity.setConnected(false);\n continue;\n }\n if (connectivity.isConnected) continue;\n\n const stabilized = yield* race([\n (function* () {\n yield* sleep(ONLINE_STABILIZATION_MS);\n return navigator.onLine;\n })(),\n (function* () {\n yield* once(window, \"offline\");\n return false;\n })(),\n ]);\n connectivity.setConnected(stabilized);\n }\n });\n void connectivity.lifetime.catch(() => undefined);\n return connectivity;\n }\n\n get isConnected(): boolean {\n return this.connected;\n }\n\n subscribe(listener: (isConnected: boolean) => void): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n close(): Promise<void> {\n this.closePromise ??= Promise.resolve(this.lifetime.halt()).finally(() => {\n this.listeners.clear();\n });\n return this.closePromise;\n }\n\n private setConnected(isConnected: boolean): void {\n if (isConnected === this.connected) return;\n this.connected = isConnected;\n for (const listener of this.listeners) {\n listener(isConnected);\n }\n }\n}\n"],"mappings":";;AAGA,MAAM,0BAA0B;AAEhC,IAAa,+BAAb,MAAa,6BAA4D;CACrE,4BAA6B,IAAI,IAAoC;CACrE;CACA;CACA;CAEA,YAAoB,oBAA6B;EAC7C,KAAK,YAAY;CACrB;CAEA,OAAO,SAAuC;EAC1C,MAAM,eAAe,IAAI,6BAA6B,UAAU,MAAM;EACtE,aAAa,WAAW,IAAI,aAAa;GACrC,OAAO,MAAM;IAWT,IAAI,EAAC,OAViB,KAAK,EACtB,aAAa;KACV,OAAO,KAAK,QAAQ,QAAQ;KAC5B,OAAO;IACX,EAAA,CAAG,IACF,aAAa;KACV,OAAO,KAAK,QAAQ,SAAS;KAC7B,OAAO;IACX,EAAA,CAAG,CACP,CAAC,IACY;KACT,aAAa,aAAa,KAAK;KAC/B;IACJ;IACA,IAAI,aAAa,aAAa;IAE9B,MAAM,aAAa,OAAO,KAAK,EAC1B,aAAa;KACV,OAAO,MAAM,uBAAuB;KACpC,OAAO,UAAU;IACrB,EAAA,CAAG,IACF,aAAa;KACV,OAAO,KAAK,QAAQ,SAAS;KAC7B,OAAO;IACX,EAAA,CAAG,CACP,CAAC;IACD,aAAa,aAAa,UAAU;GACxC;EACJ,CAAC;EACD,aAAkB,SAAS,YAAY,KAAA,CAAS;EAChD,OAAO;CACX;CAEA,IAAI,cAAuB;EACvB,OAAO,KAAK;CAChB;CAEA,UAAU,UAAsD;EAC5D,KAAK,UAAU,IAAI,QAAQ;EAC3B,aAAa;GACT,KAAK,UAAU,OAAO,QAAQ;EAClC;CACJ;CAEA,QAAuB;EACnB,KAAK,iBAAiB,QAAQ,QAAQ,KAAK,SAAS,KAAK,CAAC,CAAC,CAAC,cAAc;GACtE,KAAK,UAAU,MAAM;EACzB,CAAC;EACD,OAAO,KAAK;CAChB;CAEA,aAAqB,aAA4B;EAC7C,IAAI,gBAAgB,KAAK,WAAW;EACpC,KAAK,YAAY;EACjB,KAAK,MAAM,YAAY,KAAK,WACxB,SAAS,WAAW;CAE5B;AACJ"}
1
+ {"version":3,"file":"NavigatorNetworkConnectivity.js","names":[],"sources":["../src/NavigatorNetworkConnectivity.ts"],"sourcesContent":["import { makeNetworkConnectivity } from \"@party-stack/runtime\";\nimport { Effect, PubSub, Stream } from \"effect\";\n\nconst WORKER_CONNECTIVITY_SAMPLE_MS = 1_000;\n\n/** Native listeners and the worker event fallback belong to the caller's platform scope. */\nexport const makeNavigatorNetworkConnectivity = Effect.fnUntraced(function* () {\n const state = yield* makeNetworkConnectivity(navigator.onLine !== false);\n const target = typeof window === \"undefined\" ? globalThis : window;\n const events = yield* Effect.acquireRelease(\n PubSub.sliding<void>({ capacity: 1, replay: 1 }),\n PubSub.shutdown\n );\n let observed = navigator.onLine !== false;\n const changed = () => {\n observed = navigator.onLine !== false;\n PubSub.publishUnsafe(events, undefined);\n };\n const sample = () => {\n if (observed !== (navigator.onLine !== false)) changed();\n };\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n target.addEventListener(\"online\", changed);\n target.addEventListener(\"offline\", changed);\n }),\n () =>\n Effect.sync(() => {\n target.removeEventListener(\"online\", changed);\n target.removeEventListener(\"offline\", changed);\n })\n );\n const changes = Stream.fromPubSub(events).pipe(\n Stream.mapEffect(() => state.set(navigator.onLine !== false))\n );\n yield* Stream.runDrain(changes).pipe(Effect.forkScoped({ startImmediately: true }));\n // SharedWorkers can observe onLine changes without receiving native events\n // (including Chromium's emulated offline mode). Sample only the local flag.\n if (typeof window === \"undefined\")\n yield* Effect.forever(\n Effect.sleep(WORKER_CONNECTIVITY_SAMPLE_MS).pipe(Effect.andThen(Effect.sync(sample)))\n ).pipe(Effect.forkScoped);\n return {\n online: Effect.suspend(() => {\n sample();\n // Record an observed state before returning it. Otherwise a\n // quick reconnect can overtake the event consumer: it sees true -> true,\n // and an outbox that just paused never receives its reconnect wakeup.\n const online = navigator.onLine !== false;\n return state.set(online).pipe(Effect.as(online));\n }),\n changes: state.changes,\n };\n});\n"],"mappings":";;;AAGA,MAAM,gCAAgC;;AAGtC,MAAa,mCAAmC,OAAO,WAAW,aAAa;CAC3E,MAAM,QAAQ,OAAO,wBAAwB,UAAU,WAAW,KAAK;CACvE,MAAM,SAAS,OAAO,WAAW,cAAc,aAAa;CAC5D,MAAM,SAAS,OAAO,OAAO,eACzB,OAAO,QAAc;EAAE,UAAU;EAAG,QAAQ;CAAE,CAAC,GAC/C,OAAO,QACX;CACA,IAAI,WAAW,UAAU,WAAW;CACpC,MAAM,gBAAgB;EAClB,WAAW,UAAU,WAAW;EAChC,OAAO,cAAc,QAAQ,KAAA,CAAS;CAC1C;CACA,MAAM,eAAe;EACjB,IAAI,cAAc,UAAU,WAAW,QAAQ,QAAQ;CAC3D;CACA,OAAO,OAAO,eACV,OAAO,WAAW;EACd,OAAO,iBAAiB,UAAU,OAAO;EACzC,OAAO,iBAAiB,WAAW,OAAO;CAC9C,CAAC,SAEG,OAAO,WAAW;EACd,OAAO,oBAAoB,UAAU,OAAO;EAC5C,OAAO,oBAAoB,WAAW,OAAO;CACjD,CAAC,CACT;CACA,MAAM,UAAU,OAAO,WAAW,MAAM,CAAC,CAAC,KACtC,OAAO,gBAAgB,MAAM,IAAI,UAAU,WAAW,KAAK,CAAC,CAChE;CACA,OAAO,OAAO,SAAS,OAAO,CAAC,CAAC,KAAK,OAAO,WAAW,EAAE,kBAAkB,KAAK,CAAC,CAAC;CAGlF,IAAI,OAAO,WAAW,aAClB,OAAO,OAAO,QACV,OAAO,MAAM,6BAA6B,CAAC,CAAC,KAAK,OAAO,QAAQ,OAAO,KAAK,MAAM,CAAC,CAAC,CACxF,CAAC,CAAC,KAAK,OAAO,UAAU;CAC5B,OAAO;EACH,QAAQ,OAAO,cAAc;GACzB,OAAO;GAIP,MAAM,SAAS,UAAU,WAAW;GACpC,OAAO,MAAM,IAAI,MAAM,CAAC,CAAC,KAAK,OAAO,GAAG,MAAM,CAAC;EACnD,CAAC;EACD,SAAS,MAAM;CACnB;AACJ,CAAC"}
@@ -1,15 +1,16 @@
1
- import { BlobBytesStore } from "@party-stack/runtime";
1
+ import { BlobBytesService, ByteInput } from "@party-stack/runtime";
2
+ import { Effect } from "effect";
2
3
  //#region src/OPFSBlobBytesStore.d.ts
3
4
  export interface OPFSBlobBytesStoreOptions {
4
5
  directoryName: string;
5
6
  }
6
- export declare class OPFSBlobBytesStore implements BlobBytesStore {
7
+ export declare class OPFSBlobBytesStore implements BlobBytesService {
7
8
  #private;
9
+ readonly write: (id: string, value: ByteInput) => Effect.Effect<void, unknown, never>;
10
+ readonly read: (id: string) => Effect.Effect<import("@party-stack/runtime").ByteSource | undefined, import("effect/Cause").UnknownError, never>;
11
+ readonly delete: (id: string) => Effect.Effect<void, import("effect/Cause").UnknownError, never>;
12
+ readonly clear: () => Effect.Effect<void, import("effect/Cause").UnknownError, never>;
8
13
  constructor(options: OPFSBlobBytesStoreOptions);
9
- write(id: string, blob: Blob): Promise<void>;
10
- read(id: string): Promise<Blob>;
11
- delete(id: string): Promise<void>;
12
- clear(): Promise<void>;
13
14
  }
14
15
  //#endregion
15
16
  //# sourceMappingURL=OPFSBlobBytesStore.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"OPFSBlobBytesStore.d.ts","names":[],"sources":["../src/OPFSBlobBytesStore.ts"],"mappings":";;iBAEiB;EACb;;qBAOS,8BAA8B;;EAGvC,YAAY,SAAS;EAIf,MAAM,YAAY,MAAM,OAAO;EAa/B,KAAK,aAAa,QAAQ;EAM1B,OAAO,aAAa;EAYpB,SAAS"}
1
+ {"version":3,"file":"OPFSBlobBytesStore.d.ts","names":[],"sources":["../src/OPFSBlobBytesStore.ts"],"mappings":";;;iBAGiB;EACb;;qBAsBS,8BAA8B;;WAG9B,QAAK,YAAc,OAAS,cAAS,OAAA;WACrC,OAAI,eAAc,OAAA,sCAAA,+CAAA;WAIlB,SAAM,eAAc,OAAA,oCAAA;WACpB,aAAK,OAAA,oCAAA;EAEd,YAAY,SAAS"}
@@ -1,43 +1,57 @@
1
+ import { byteIO, byteSource } from "@party-stack/runtime";
2
+ import { Effect, Stream } from "effect";
1
3
  //#region src/OPFSBlobBytesStore.ts
2
- function encodeFileName(id) {
3
- return encodeURIComponent(id);
4
+ /** Preserve existing valid names. Composite partition keys contain JSON backslashes,
5
+ * which OPFS rejects; hashes also keep addresses within filesystem component limits.
6
+ * The reserved # prefix cannot collide with URI-encoded file IDs or un-hashed directories.
7
+ */
8
+ async function opfsName(name) {
9
+ const bytes = new TextEncoder().encode(name);
10
+ if (bytes.length > 0 && bytes.length <= 255 && !/[\\/\0]/.test(name) && name !== "." && name !== ".." && !name.startsWith("#")) return name;
11
+ const hash = await crypto.subtle.digest("SHA-256", bytes);
12
+ return `#${Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
4
13
  }
5
14
  var OPFSBlobBytesStore = class {
6
15
  #directoryName;
16
+ write = (id, value) => byteIO((signal) => this.#write(id, value, signal));
17
+ read = (id) => Effect.tryPromise(() => this.#read(id)).pipe(Effect.map((value) => value === void 0 ? void 0 : byteSource(value)));
18
+ delete = (id) => Effect.tryPromise(() => this.#delete(id));
19
+ clear = () => Effect.tryPromise(() => this.#clear());
7
20
  constructor(options) {
8
21
  this.#directoryName = options.directoryName;
9
22
  }
10
- async write(id, blob) {
11
- const writable = await (await (await this.#directory()).getFileHandle(encodeFileName(id), { create: true })).createWritable();
23
+ async #write(id, input, signal) {
24
+ const writable = await (await (await this.#directory()).getFileHandle(await opfsName(encodeURIComponent(id)), { create: true })).createWritable();
25
+ await Stream.toReadableStream(byteSource(input).stream).pipeTo(writable, { signal });
26
+ }
27
+ async #read(id) {
12
28
  try {
13
- await writable.write(blob);
14
- } finally {
15
- await writable.close();
29
+ return await (await (await this.#directory()).getFileHandle(await opfsName(encodeURIComponent(id)))).getFile();
30
+ } catch (cause) {
31
+ if (cause instanceof DOMException && cause.name === "NotFoundError") return void 0;
32
+ throw cause;
16
33
  }
17
34
  }
18
- async read(id) {
19
- return (await (await this.#directory()).getFileHandle(encodeFileName(id))).getFile();
20
- }
21
- async delete(id) {
35
+ async #delete(id) {
22
36
  const directory = await this.#directory();
23
37
  try {
24
- await directory.removeEntry(encodeFileName(id));
38
+ await directory.removeEntry(await opfsName(encodeURIComponent(id)));
25
39
  } catch (error) {
26
40
  if (error instanceof DOMException && error.name === "NotFoundError") return;
27
41
  throw error;
28
42
  }
29
43
  }
30
- async clear() {
44
+ async #clear() {
31
45
  const root = await navigator.storage.getDirectory();
32
46
  try {
33
- await root.removeEntry(this.#directoryName, { recursive: true });
47
+ await root.removeEntry(await opfsName(this.#directoryName), { recursive: true });
34
48
  } catch (error) {
35
49
  if (error instanceof DOMException && error.name === "NotFoundError") return;
36
50
  throw error;
37
51
  }
38
52
  }
39
53
  async #directory() {
40
- return (await navigator.storage.getDirectory()).getDirectoryHandle(this.#directoryName, { create: true });
54
+ return (await navigator.storage.getDirectory()).getDirectoryHandle(await opfsName(this.#directoryName), { create: true });
41
55
  }
42
56
  };
43
57
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"OPFSBlobBytesStore.js","names":[],"sources":["../src/OPFSBlobBytesStore.ts"],"sourcesContent":["import type { BlobBytesStore } from \"@party-stack/runtime\";\n\nexport interface OPFSBlobBytesStoreOptions {\n directoryName: string;\n}\n\nfunction encodeFileName(id: string): string {\n return encodeURIComponent(id);\n}\n\nexport class OPFSBlobBytesStore implements BlobBytesStore {\n #directoryName: string;\n\n constructor(options: OPFSBlobBytesStoreOptions) {\n this.#directoryName = options.directoryName;\n }\n\n async write(id: string, blob: Blob): Promise<void> {\n const directory = await this.#directory();\n const fileHandle = await directory.getFileHandle(encodeFileName(id), {\n create: true,\n });\n const writable = await fileHandle.createWritable();\n try {\n await writable.write(blob);\n } finally {\n await writable.close();\n }\n }\n\n async read(id: string): Promise<Blob> {\n const directory = await this.#directory();\n const fileHandle = await directory.getFileHandle(encodeFileName(id));\n return fileHandle.getFile();\n }\n\n async delete(id: string): Promise<void> {\n const directory = await this.#directory();\n try {\n await directory.removeEntry(encodeFileName(id));\n } catch (error) {\n if (error instanceof DOMException && error.name === \"NotFoundError\") {\n return;\n }\n throw error;\n }\n }\n\n async clear(): Promise<void> {\n const root = await navigator.storage.getDirectory();\n try {\n await root.removeEntry(this.#directoryName, {\n recursive: true,\n });\n } catch (error) {\n if (error instanceof DOMException && error.name === \"NotFoundError\") {\n return;\n }\n throw error;\n }\n }\n\n async #directory(): Promise<FileSystemDirectoryHandle> {\n const root = await navigator.storage.getDirectory();\n return root.getDirectoryHandle(this.#directoryName, { create: true });\n }\n}\n"],"mappings":";AAMA,SAAS,eAAe,IAAoB;CACxC,OAAO,mBAAmB,EAAE;AAChC;AAEA,IAAa,qBAAb,MAA0D;CACtD;CAEA,YAAY,SAAoC;EAC5C,KAAK,iBAAiB,QAAQ;CAClC;CAEA,MAAM,MAAM,IAAY,MAA2B;EAK/C,MAAM,WAAW,OAAM,OAHE,MADD,KAAK,WAAW,EAAA,CACL,cAAc,eAAe,EAAE,GAAG,EACjE,QAAQ,KACZ,CAAC,EAAA,CACiC,eAAe;EACjD,IAAI;GACA,MAAM,SAAS,MAAM,IAAI;EAC7B,UAAU;GACN,MAAM,SAAS,MAAM;EACzB;CACJ;CAEA,MAAM,KAAK,IAA2B;EAGlC,QAAO,OADkB,MADD,KAAK,WAAW,EAAA,CACL,cAAc,eAAe,EAAE,CAAC,EAAA,CACjD,QAAQ;CAC9B;CAEA,MAAM,OAAO,IAA2B;EACpC,MAAM,YAAY,MAAM,KAAK,WAAW;EACxC,IAAI;GACA,MAAM,UAAU,YAAY,eAAe,EAAE,CAAC;EAClD,SAAS,OAAO;GACZ,IAAI,iBAAiB,gBAAgB,MAAM,SAAS,iBAChD;GAEJ,MAAM;EACV;CACJ;CAEA,MAAM,QAAuB;EACzB,MAAM,OAAO,MAAM,UAAU,QAAQ,aAAa;EAClD,IAAI;GACA,MAAM,KAAK,YAAY,KAAK,gBAAgB,EACxC,WAAW,KACf,CAAC;EACL,SAAS,OAAO;GACZ,IAAI,iBAAiB,gBAAgB,MAAM,SAAS,iBAChD;GAEJ,MAAM;EACV;CACJ;CAEA,MAAM,aAAiD;EAEnD,QAAO,MADY,UAAU,QAAQ,aAAa,EAAA,CACtC,mBAAmB,KAAK,gBAAgB,EAAE,QAAQ,KAAK,CAAC;CACxE;AACJ"}
1
+ {"version":3,"file":"OPFSBlobBytesStore.js","names":[],"sources":["../src/OPFSBlobBytesStore.ts"],"sourcesContent":["import { byteSource, type ByteInput, byteIO, type BlobBytesService } from \"@party-stack/runtime\";\nimport { Effect, Stream } from \"effect\";\n\nexport interface OPFSBlobBytesStoreOptions {\n directoryName: string;\n}\n\n/** Preserve existing valid names. Composite partition keys contain JSON backslashes,\n * which OPFS rejects; hashes also keep addresses within filesystem component limits.\n * The reserved # prefix cannot collide with URI-encoded file IDs or un-hashed directories.\n */\nasync function opfsName(name: string): Promise<string> {\n const bytes = new TextEncoder().encode(name);\n if (\n bytes.length > 0 &&\n bytes.length <= 255 &&\n !/[\\\\/\\0]/.test(name) &&\n name !== \".\" &&\n name !== \"..\" &&\n !name.startsWith(\"#\")\n )\n return name;\n const hash = await crypto.subtle.digest(\"SHA-256\", bytes);\n return `#${Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\nexport class OPFSBlobBytesStore implements BlobBytesService {\n #directoryName: string;\n\n readonly write = (id: string, value: ByteInput) => byteIO((signal) => this.#write(id, value, signal));\n readonly read = (id: string) =>\n Effect.tryPromise(() => this.#read(id)).pipe(\n Effect.map((value) => (value === undefined ? undefined : byteSource(value)))\n );\n readonly delete = (id: string) => Effect.tryPromise(() => this.#delete(id));\n readonly clear = () => Effect.tryPromise(() => this.#clear());\n\n constructor(options: OPFSBlobBytesStoreOptions) {\n this.#directoryName = options.directoryName;\n }\n\n async #write(id: string, input: ByteInput, signal?: AbortSignal): Promise<void> {\n const directory = await this.#directory();\n const fileHandle = await directory.getFileHandle(await opfsName(encodeURIComponent(id)), {\n create: true,\n });\n const writable = await fileHandle.createWritable();\n await Stream.toReadableStream(byteSource(input).stream).pipeTo(writable, { signal });\n }\n\n async #read(id: string): Promise<Blob | undefined> {\n try {\n const directory = await this.#directory();\n const fileHandle = await directory.getFileHandle(await opfsName(encodeURIComponent(id)));\n return await fileHandle.getFile();\n } catch (cause) {\n if (cause instanceof DOMException && cause.name === \"NotFoundError\") return undefined;\n throw cause;\n }\n }\n\n async #delete(id: string): Promise<void> {\n const directory = await this.#directory();\n try {\n await directory.removeEntry(await opfsName(encodeURIComponent(id)));\n } catch (error) {\n if (error instanceof DOMException && error.name === \"NotFoundError\") {\n return;\n }\n throw error;\n }\n }\n\n async #clear(): Promise<void> {\n const root = await navigator.storage.getDirectory();\n try {\n await root.removeEntry(await opfsName(this.#directoryName), {\n recursive: true,\n });\n } catch (error) {\n if (error instanceof DOMException && error.name === \"NotFoundError\") {\n return;\n }\n throw error;\n }\n }\n\n async #directory(): Promise<FileSystemDirectoryHandle> {\n const root = await navigator.storage.getDirectory();\n return root.getDirectoryHandle(await opfsName(this.#directoryName), { create: true });\n }\n}\n"],"mappings":";;;;;;;AAWA,eAAe,SAAS,MAA+B;CACnD,MAAM,QAAQ,IAAI,YAAY,CAAC,CAAC,OAAO,IAAI;CAC3C,IACI,MAAM,SAAS,KACf,MAAM,UAAU,OAChB,CAAC,UAAU,KAAK,IAAI,KACpB,SAAS,OACT,SAAS,QACT,CAAC,KAAK,WAAW,GAAG,GAEpB,OAAO;CACX,MAAM,OAAO,MAAM,OAAO,OAAO,OAAO,WAAW,KAAK;CACxD,OAAO,IAAI,MAAM,KAAK,IAAI,WAAW,IAAI,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;AACrG;AAEA,IAAa,qBAAb,MAA4D;CACxD;CAEA,SAAkB,IAAY,UAAqB,QAAQ,WAAW,KAAK,OAAO,IAAI,OAAO,MAAM,CAAC;CACpG,QAAiB,OACb,OAAO,iBAAiB,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC,KACpC,OAAO,KAAK,UAAW,UAAU,KAAA,IAAY,KAAA,IAAY,WAAW,KAAK,CAAE,CAC/E;CACJ,UAAmB,OAAe,OAAO,iBAAiB,KAAK,QAAQ,EAAE,CAAC;CAC1E,cAAuB,OAAO,iBAAiB,KAAK,OAAO,CAAC;CAE5D,YAAY,SAAoC;EAC5C,KAAK,iBAAiB,QAAQ;CAClC;CAEA,MAAM,OAAO,IAAY,OAAkB,QAAqC;EAK5E,MAAM,WAAW,OAAM,OAHE,MADD,KAAK,WAAW,EAAA,CACL,cAAc,MAAM,SAAS,mBAAmB,EAAE,CAAC,GAAG,EACrF,QAAQ,KACZ,CAAC,EAAA,CACiC,eAAe;EACjD,MAAM,OAAO,iBAAiB,WAAW,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,UAAU,EAAE,OAAO,CAAC;CACvF;CAEA,MAAM,MAAM,IAAuC;EAC/C,IAAI;GAGA,OAAO,OAAM,OADY,MADD,KAAK,WAAW,EAAA,CACL,cAAc,MAAM,SAAS,mBAAmB,EAAE,CAAC,CAAC,EAAA,CAC/D,QAAQ;EACpC,SAAS,OAAO;GACZ,IAAI,iBAAiB,gBAAgB,MAAM,SAAS,iBAAiB,OAAO,KAAA;GAC5E,MAAM;EACV;CACJ;CAEA,MAAM,QAAQ,IAA2B;EACrC,MAAM,YAAY,MAAM,KAAK,WAAW;EACxC,IAAI;GACA,MAAM,UAAU,YAAY,MAAM,SAAS,mBAAmB,EAAE,CAAC,CAAC;EACtE,SAAS,OAAO;GACZ,IAAI,iBAAiB,gBAAgB,MAAM,SAAS,iBAChD;GAEJ,MAAM;EACV;CACJ;CAEA,MAAM,SAAwB;EAC1B,MAAM,OAAO,MAAM,UAAU,QAAQ,aAAa;EAClD,IAAI;GACA,MAAM,KAAK,YAAY,MAAM,SAAS,KAAK,cAAc,GAAG,EACxD,WAAW,KACf,CAAC;EACL,SAAS,OAAO;GACZ,IAAI,iBAAiB,gBAAgB,MAAM,SAAS,iBAChD;GAEJ,MAAM;EACV;CACJ;CAEA,MAAM,aAAiD;EAEnD,QAAO,MADY,UAAU,QAAQ,aAAa,EAAA,CACtC,mBAAmB,MAAM,SAAS,KAAK,cAAc,GAAG,EAAE,QAAQ,KAAK,CAAC;CACxF;AACJ"}
@@ -0,0 +1,30 @@
1
+ import { Runtime, RuntimeAcquisitionError, RuntimePartition, Runtimes } from "@party-stack/runtime";
2
+ import { Effect, Layer, Scope } from "effect";
3
+ import { TablesService } from "@party-stack/ontology-storage/tables";
4
+ //#region src/WebRuntime.d.ts
5
+ export interface WebRuntimeOptions {
6
+ /** Selects the built-in tables and ancillary blob/keyring/capacity persistence. */
7
+ readonly storage?: "indexeddb" | "memory";
8
+ /** Replace table acquisition without changing browser capabilities or opening built-in tables.
9
+ * The acquisition Scope owns open resources. Destroy must also work after that Scope closes;
10
+ * it deletes this partition's custom tables when the replica is forgotten.
11
+ */
12
+ readonly tables?: Effect.Effect<{
13
+ readonly tables: TablesService;
14
+ readonly destroy: Effect.Effect<void, RuntimeAcquisitionError>;
15
+ }, RuntimeAcquisitionError, RuntimePartition | Scope.Scope>;
16
+ /** Explicitly allow a non-extractable AES-GCM CryptoKey to persist in IndexedDB across reloads.
17
+ * Same-origin code can still use it to decrypt: this is not a vault lock or protection from XSS.
18
+ * Keys are separate from ordinary table exports. Clearing site data loses the keys and makes
19
+ * remaining local ciphertext unreadable. This flag does not opt encrypted properties into caching;
20
+ * individual decrypt calls must request cache:true. Omitted keys must be composed separately.
21
+ */
22
+ readonly persistEncryptionKeys?: boolean;
23
+ }
24
+ /** Lazy platform acquisition. The containing Layer Scope owns all open resources. */
25
+ export declare const make: (options?: WebRuntimeOptions | undefined) => Effect.Effect<import("@party-stack/runtime").RuntimeService, RuntimeAcquisitionError, RuntimePartition | Scope.Scope>;
26
+ export declare const layer: (options?: WebRuntimeOptions) => Layer.Layer<Runtime, RuntimeAcquisitionError, RuntimePartition>;
27
+ /** Browser capabilities are available before any partition storage is acquired. */
28
+ export declare const webRuntime: (options?: WebRuntimeOptions) => Layer.Layer<Runtimes, never, never>;
29
+ //#endregion
30
+ //# sourceMappingURL=WebRuntime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebRuntime.d.ts","names":[],"sources":["../src/WebRuntime.ts"],"mappings":";;;;iBAciB;;WAEJ;;;;;WAKA,SAAS,OAAO;aACV,QAAQ;aAAwB,SAAS,OAAO,aAAa;KACxE,yBACA,mBAAmB,MAAM;;;;;;;WAQpB;;;qBAIA,OAAI,UAAA,kCAAA,OAAA,sCAAA,gBAAA,yBAAA,mBAAA,MAAA;qBAoFJ,QAAK,UAAa,sBAAiB,MAAA,MAAA,SAAA,yBAAA;;qBAGnC,aAAU,UAAa,sBAAiB,MAAA,MAAA"}
@@ -0,0 +1,83 @@
1
+ import { makeNavigatorNetworkConnectivity } from "./NavigatorNetworkConnectivity.js";
2
+ import { OPFSBlobBytesStore } from "./OPFSBlobBytesStore.js";
3
+ import { createWebBrowserAuthentication } from "./createWebBrowserAuthentication.js";
4
+ import { MemoryBlobBytesStore, Runtime, RuntimeError, RuntimePartition, Runtimes, makeMemoryEncryptionKeyring } from "@party-stack/runtime";
5
+ import { Effect, Layer } from "effect";
6
+ import * as IndexedDb from "@effect/platform-browser/IndexedDb";
7
+ import { makeEncryptionKeyring } from "@party-stack/indexeddb-ontology-storage/encryption-keyring";
8
+ import * as IndexedDbTables from "@party-stack/indexeddb-ontology-storage/tables";
9
+ import * as Memory from "@party-stack/ontology-storage/memory/tables";
10
+ //#region src/WebRuntime.ts
11
+ /** Lazy platform acquisition. The containing Layer Scope owns all open resources. */
12
+ const make = Effect.fn("WebRuntime.make")(function* (options = {}) {
13
+ const { owner, namespace } = yield* RuntimePartition;
14
+ const persistent = options.storage !== "memory";
15
+ if (persistent && (!globalThis.indexedDB || !globalThis.IDBKeyRange || !globalThis.navigator?.storage?.getDirectory)) return yield* new RuntimeError({
16
+ operation: "webRuntime.storage",
17
+ cause: /* @__PURE__ */ new Error("IndexedDB and OPFS are unavailable. Isolated memory storage can be selected explicitly.")
18
+ });
19
+ const name = `party-stack:${JSON.stringify([owner, namespace])}`;
20
+ const connectivity = yield* makeNavigatorNetworkConnectivity();
21
+ const indexedDb = Layer.sync(IndexedDb.IndexedDb)(() => IndexedDb.make({
22
+ indexedDB: globalThis.indexedDB,
23
+ IDBKeyRange: globalThis.IDBKeyRange
24
+ }));
25
+ const blobBytes = persistent ? new OPFSBlobBytesStore({ directoryName: `${name}:blobs` }) : new MemoryBlobBytesStore();
26
+ const customTables = options.tables ? yield* options.tables : void 0;
27
+ const adapter = {
28
+ capacity: {
29
+ memory: Effect.succeed(void 0),
30
+ storage: persistent ? Effect.tryPromise({
31
+ try: async () => {
32
+ if (!globalThis.navigator?.storage?.estimate) return void 0;
33
+ const estimate = await navigator.storage.estimate();
34
+ return {
35
+ scope: "origin",
36
+ usedBytes: estimate.usage,
37
+ limitBytes: estimate.quota,
38
+ availableBytes: estimate.quota === void 0 || estimate.usage === void 0 ? void 0 : Math.max(0, estimate.quota - estimate.usage)
39
+ };
40
+ },
41
+ catch: (cause) => new RuntimeError({
42
+ operation: "capacity.storage",
43
+ cause
44
+ })
45
+ }) : Effect.succeed(void 0)
46
+ },
47
+ connectivity,
48
+ blobBytes,
49
+ tables: customTables?.tables ?? (persistent ? yield* IndexedDbTables.make({ databaseName: `${name}:storage` }).pipe(Effect.provide(indexedDb)) : yield* Memory.make()),
50
+ destroy: persistent || customTables ? Effect.all([
51
+ customTables?.destroy ?? (persistent ? Effect.all([removeDatabase(name), removeDatabase(`${name}:storage`)], { concurrency: "unbounded" }) : Effect.void),
52
+ persistent ? removeDatabase(`${name}:keys`) : Effect.void,
53
+ blobBytes instanceof OPFSBlobBytesStore ? blobBytes.clear() : Effect.void
54
+ ], { concurrency: "unbounded" }).pipe(Effect.asVoid, Effect.mapError((cause) => new RuntimeError({
55
+ operation: "destroy",
56
+ cause
57
+ }))) : void 0,
58
+ encryptionKeyring: persistent && options.persistEncryptionKeys ? yield* makeEncryptionKeyring(`${name}:keys`).pipe(Effect.provide(indexedDb)) : persistent ? void 0 : yield* makeMemoryEncryptionKeyring(),
59
+ browserAuthentication: typeof window !== "undefined" ? createWebBrowserAuthentication() : void 0
60
+ };
61
+ return yield* Runtime.make(adapter);
62
+ });
63
+ const layer = (options = {}) => Layer.effect(Runtime, make(options));
64
+ /** Browser capabilities are available before any partition storage is acquired. */
65
+ const webRuntime = (options = {}) => Runtimes.layer(layer(options), { browserAuthentication: typeof window !== "undefined" ? createWebBrowserAuthentication() : void 0 });
66
+ function deleteDatabase(name) {
67
+ return new Promise((resolve, reject) => {
68
+ const request = globalThis.indexedDB.deleteDatabase(name);
69
+ request.onsuccess = () => resolve();
70
+ request.onerror = () => reject(new Error(`IndexedDB database "${name}" could not be deleted.`, { cause: request.error }));
71
+ });
72
+ }
73
+ const removeDatabase = (name) => Effect.tryPromise({
74
+ try: () => deleteDatabase(name),
75
+ catch: (cause) => new RuntimeError({
76
+ operation: "destroy",
77
+ cause
78
+ })
79
+ });
80
+ //#endregion
81
+ export { layer, make, webRuntime };
82
+
83
+ //# sourceMappingURL=WebRuntime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebRuntime.js","names":[],"sources":["../src/WebRuntime.ts"],"sourcesContent":["import * as IndexedDb from \"@effect/platform-browser/IndexedDb\";\nimport { makeEncryptionKeyring } from \"@party-stack/indexeddb-ontology-storage/encryption-keyring\";\nimport * as IndexedDbTables from \"@party-stack/indexeddb-ontology-storage/tables\";\nimport * as Memory from \"@party-stack/ontology-storage/memory/tables\";\nimport { makeMemoryEncryptionKeyring, MemoryBlobBytesStore } from \"@party-stack/runtime\";\nimport { Runtime, RuntimeError, RuntimePartition, Runtimes } from \"@party-stack/runtime\";\nimport { Effect, Layer } from \"effect\";\nimport { createWebBrowserAuthentication } from \"./createWebBrowserAuthentication.js\";\nimport { makeNavigatorNetworkConnectivity } from \"./NavigatorNetworkConnectivity.js\";\nimport { OPFSBlobBytesStore } from \"./OPFSBlobBytesStore.js\";\nimport type { TablesService } from \"@party-stack/ontology-storage/tables\";\nimport type { RuntimeOptions, RuntimeAcquisitionError } from \"@party-stack/runtime\";\nimport type { Scope } from \"effect\";\n\nexport interface WebRuntimeOptions {\n /** Selects the built-in tables and ancillary blob/keyring/capacity persistence. */\n readonly storage?: \"indexeddb\" | \"memory\";\n /** Replace table acquisition without changing browser capabilities or opening built-in tables.\n * The acquisition Scope owns open resources. Destroy must also work after that Scope closes;\n * it deletes this partition's custom tables when the replica is forgotten.\n */\n readonly tables?: Effect.Effect<\n { readonly tables: TablesService; readonly destroy: Effect.Effect<void, RuntimeAcquisitionError> },\n RuntimeAcquisitionError,\n RuntimePartition | Scope.Scope\n >;\n /** Explicitly allow a non-extractable AES-GCM CryptoKey to persist in IndexedDB across reloads.\n * Same-origin code can still use it to decrypt: this is not a vault lock or protection from XSS.\n * Keys are separate from ordinary table exports. Clearing site data loses the keys and makes\n * remaining local ciphertext unreadable. This flag does not opt encrypted properties into caching;\n * individual decrypt calls must request cache:true. Omitted keys must be composed separately.\n */\n readonly persistEncryptionKeys?: boolean;\n}\n\n/** Lazy platform acquisition. The containing Layer Scope owns all open resources. */\nexport const make = Effect.fn(\"WebRuntime.make\")(function* (options: WebRuntimeOptions = {}) {\n const { owner, namespace } = yield* RuntimePartition;\n const persistent = options.storage !== \"memory\";\n if (\n persistent &&\n (!globalThis.indexedDB || !globalThis.IDBKeyRange || !globalThis.navigator?.storage?.getDirectory)\n )\n return yield* new RuntimeError({\n operation: \"webRuntime.storage\",\n cause: new Error(\n \"IndexedDB and OPFS are unavailable. Isolated memory storage can be selected explicitly.\"\n ),\n });\n const name = `party-stack:${JSON.stringify([owner, namespace])}`;\n const connectivity = yield* makeNavigatorNetworkConnectivity();\n // Effect's layerWindow reads window; supplying the factory explicitly also works in workers.\n const indexedDb = Layer.sync(IndexedDb.IndexedDb)(() =>\n IndexedDb.make({ indexedDB: globalThis.indexedDB, IDBKeyRange: globalThis.IDBKeyRange })\n );\n const blobBytes = persistent\n ? new OPFSBlobBytesStore({ directoryName: `${name}:blobs` })\n : new MemoryBlobBytesStore();\n const customTables = options.tables ? yield* options.tables : undefined;\n const adapter: RuntimeOptions = {\n capacity: {\n memory: Effect.succeed(undefined),\n storage: persistent\n ? Effect.tryPromise({\n try: async () => {\n if (!globalThis.navigator?.storage?.estimate) return undefined;\n const estimate = await navigator.storage.estimate();\n return {\n scope: \"origin\" as const,\n usedBytes: estimate.usage,\n limitBytes: estimate.quota,\n availableBytes:\n estimate.quota === undefined || estimate.usage === undefined\n ? undefined\n : Math.max(0, estimate.quota - estimate.usage),\n };\n },\n catch: (cause) => new RuntimeError({ operation: \"capacity.storage\", cause }),\n })\n : Effect.succeed(undefined),\n },\n connectivity,\n blobBytes,\n tables:\n customTables?.tables ??\n (persistent\n ? yield* IndexedDbTables.make({ databaseName: `${name}:storage` }).pipe(\n Effect.provide(indexedDb)\n )\n : yield* Memory.make()),\n destroy:\n persistent || customTables\n ? Effect.all(\n [\n customTables?.destroy ??\n (persistent\n ? Effect.all([removeDatabase(name), removeDatabase(`${name}:storage`)], {\n concurrency: \"unbounded\",\n })\n : Effect.void),\n persistent ? removeDatabase(`${name}:keys`) : Effect.void,\n blobBytes instanceof OPFSBlobBytesStore ? blobBytes.clear() : Effect.void,\n ],\n { concurrency: \"unbounded\" }\n ).pipe(\n Effect.asVoid,\n Effect.mapError((cause) => new RuntimeError({ operation: \"destroy\", cause }))\n )\n : undefined,\n encryptionKeyring:\n persistent && options.persistEncryptionKeys\n ? yield* makeEncryptionKeyring(`${name}:keys`).pipe(Effect.provide(indexedDb))\n : persistent\n ? undefined\n : yield* makeMemoryEncryptionKeyring(),\n browserAuthentication: typeof window !== \"undefined\" ? createWebBrowserAuthentication() : undefined,\n };\n return yield* Runtime.make(adapter);\n});\n\nexport const layer = (options: WebRuntimeOptions = {}) => Layer.effect(Runtime, make(options));\n\n/** Browser capabilities are available before any partition storage is acquired. */\nexport const webRuntime = (options: WebRuntimeOptions = {}) =>\n Runtimes.layer(layer(options), {\n browserAuthentication: typeof window !== \"undefined\" ? createWebBrowserAuthentication() : undefined,\n });\n\nfunction deleteDatabase(name: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const request = globalThis.indexedDB.deleteDatabase(name);\n request.onsuccess = () => resolve();\n request.onerror = () =>\n reject(new Error(`IndexedDB database \"${name}\" could not be deleted.`, { cause: request.error }));\n // \"blocked\" is a progress event: cooperating connections may still be\n // finishing transactions after close()/versionchange. The deletion is\n // already queued and cannot be cancelled, so await its final result.\n });\n}\n\nconst removeDatabase = (name: string) =>\n Effect.tryPromise({\n try: () => deleteDatabase(name),\n catch: (cause) => new RuntimeError({ operation: \"destroy\", cause }),\n });\n"],"mappings":"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"}
@@ -1,5 +1,13 @@
1
1
  import { BrowserAuthentication } from "@party-stack/runtime";
2
2
  //#region src/createWebBrowserAuthentication.d.ts
3
+ /** Call on the redirect page. A different opener origin must be explicitly configured;
4
+ * for example, a registered localhost callback returning to a named dev worktree.
5
+ */
6
+ export declare function relayWebBrowserAuthenticationCallback(callbackUrl?: string, options?: {
7
+ openerOrigin?: string;
8
+ onAccepted?: () => void;
9
+ }): (() => void) | undefined;
10
+ /** The acquisition runs synchronously on web: reserve before any OAuth/RPC await. */
3
11
  export declare function createWebBrowserAuthentication(options?: {
4
12
  popupTarget?: string;
5
13
  popupFeatures?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"createWebBrowserAuthentication.d.ts","names":[],"sources":["../src/createWebBrowserAuthentication.ts"],"mappings":";;wBAwBgB,+BACZ;EACI;EACA;IAEL"}
1
+ {"version":3,"file":"createWebBrowserAuthentication.d.ts","names":[],"sources":["../src/createWebBrowserAuthentication.ts"],"mappings":";;;;;wBAsBgB,sCACZ,sBACA;EAAW;EAAuB;;;wBAyCtB,+BACZ;EAAW;EAAsB;IAClC"}
@@ -1,66 +1,163 @@
1
- import { BrowserAuthenticationCancelledError } from "@party-stack/runtime";
1
+ import { BrowserAuthenticationError } from "@party-stack/runtime";
2
+ import { Effect, Schema } from "effect";
2
3
  //#region src/createWebBrowserAuthentication.ts
4
+ const callbackMessage = Schema.Struct({
5
+ type: Schema.Literal("party-stack:browser-authentication:callback"),
6
+ callbackUrl: Schema.String
7
+ });
8
+ const isCallbackMessage = Schema.is(callbackMessage);
9
+ const acceptedMessage = Schema.Struct({
10
+ type: Schema.Literal("party-stack:browser-authentication:accepted"),
11
+ state: Schema.NullOr(Schema.String)
12
+ });
13
+ const isAcceptedMessage = Schema.is(acceptedMessage);
14
+ /** Call on the redirect page. A different opener origin must be explicitly configured;
15
+ * for example, a registered localhost callback returning to a named dev worktree.
16
+ */
17
+ function relayWebBrowserAuthenticationCallback(callbackUrl = window.location.href, options = {}) {
18
+ const opener = window.opener;
19
+ if (!opener) return;
20
+ let cleanup = () => {};
21
+ try {
22
+ const callback = new URL(callbackUrl);
23
+ if (callback.origin !== window.location.origin) return;
24
+ const openerOrigin = new URL(options.openerOrigin ?? callback.origin).origin;
25
+ const state = callback.searchParams.get("state");
26
+ const accepted = (event) => {
27
+ if (event.origin !== openerOrigin || event.source !== opener || !isAcceptedMessage(event.data) || event.data.state !== state) return;
28
+ cleanup();
29
+ options.onAccepted?.();
30
+ };
31
+ cleanup = () => window.removeEventListener("message", accepted);
32
+ window.addEventListener("message", accepted);
33
+ opener.postMessage({
34
+ type: "party-stack:browser-authentication:callback",
35
+ callbackUrl
36
+ }, openerOrigin);
37
+ return cleanup;
38
+ } catch {
39
+ cleanup();
40
+ return;
41
+ }
42
+ }
3
43
  function matchesRedirectUrl(candidate, redirectUrl) {
4
44
  const actual = new URL(candidate);
5
45
  const expected = new URL(redirectUrl);
6
46
  return actual.origin === expected.origin && actual.pathname === expected.pathname;
7
47
  }
8
- function createRedirectSession() {
9
- return {
10
- open(authorizationUrl) {
11
- location.assign(authorizationUrl);
12
- return new Promise(() => void 0);
13
- },
14
- close() {
15
- window.stop();
16
- }
17
- };
18
- }
48
+ /** The acquisition runs synchronously on web: reserve before any OAuth/RPC await. */
19
49
  function createWebBrowserAuthentication(options = {}) {
20
- return { start({ redirectUrl, presentation }) {
21
- if (presentation !== "popup") return createRedirectSession();
22
- const popup = window.open("about:blank", options.popupTarget ?? "party-stack-authentication", options.popupFeatures ?? "popup=yes,width=500,height=700");
23
- if (!popup) return createRedirectSession();
24
- let stopWaiting;
50
+ return { start: Effect.fnUntraced(function* ({ redirectUrl, presentation }) {
51
+ const popup = yield* Effect.try({
52
+ try: () => {
53
+ new URL(redirectUrl);
54
+ return presentation === "popup" ? window.open("about:blank", options.popupTarget ?? "party-stack-authentication", options.popupFeatures ?? "popup=yes,width=500,height=700") : null;
55
+ },
56
+ catch: (cause) => new BrowserAuthenticationError({
57
+ reason: "Configuration",
58
+ message: "Could not reserve browser authentication.",
59
+ cause
60
+ })
61
+ });
62
+ if (presentation === "popup" && !popup) return yield* new BrowserAuthenticationError({
63
+ reason: "Platform",
64
+ message: "The authentication popup was blocked."
65
+ });
66
+ let closed = false;
67
+ let used = false;
25
68
  let cancel;
26
- return {
27
- open(authorizationUrl) {
28
- popup.location.assign(authorizationUrl);
29
- return new Promise((resolve, reject) => {
30
- cancel = () => {
31
- stopWaiting?.();
32
- cancel = void 0;
33
- reject(new BrowserAuthenticationCancelledError());
34
- };
35
- const timer = window.setInterval(() => {
36
- if (popup.closed) {
37
- cancel?.();
38
- return;
39
- }
69
+ const close = () => {
70
+ if (closed) return;
71
+ closed = true;
72
+ cancel?.();
73
+ if (popup) popup.close();
74
+ else if (used) window.stop();
75
+ };
76
+ yield* Effect.addFinalizer(() => Effect.sync(close));
77
+ return { open: (authorizationUrl) => Effect.suspend(() => {
78
+ if (used || closed) return Effect.fail(new BrowserAuthenticationError({
79
+ reason: "Configuration",
80
+ message: "This authentication session has already been used or closed."
81
+ }));
82
+ used = true;
83
+ if (!popup) return Effect.try({
84
+ try: () => location.assign(authorizationUrl),
85
+ catch: (cause) => new BrowserAuthenticationError({
86
+ reason: "Platform",
87
+ message: "Could not open browser authorization.",
88
+ cause
89
+ })
90
+ }).pipe(Effect.andThen(Effect.never));
91
+ return Effect.callback((resume) => {
92
+ const expected = new URL(redirectUrl);
93
+ let state;
94
+ try {
95
+ state = new URL(authorizationUrl).searchParams.get("state");
96
+ } catch (cause) {
97
+ resume(Effect.fail(new BrowserAuthenticationError({
98
+ reason: "Configuration",
99
+ message: "Invalid authorization URL.",
100
+ cause
101
+ })));
102
+ return;
103
+ }
104
+ let done = false;
105
+ const finish = (result) => {
106
+ if (done) return;
107
+ done = true;
108
+ cleanup();
109
+ resume(result);
110
+ };
111
+ const accept = (candidate) => {
112
+ try {
113
+ if (!matchesRedirectUrl(candidate, redirectUrl) || new URL(candidate).searchParams.get("state") !== state) return;
40
114
  try {
41
- const current = popup.location.href;
42
- if (!matchesRedirectUrl(current, redirectUrl)) return;
43
- stopWaiting?.();
44
- cancel = void 0;
45
- popup.close();
46
- resolve({ callbackUrl: current });
115
+ popup.postMessage?.({
116
+ type: "party-stack:browser-authentication:accepted",
117
+ state
118
+ }, expected.origin);
47
119
  } catch {}
48
- }, 250);
49
- stopWaiting = () => {
50
- window.clearInterval(timer);
51
- stopWaiting = void 0;
52
- };
53
- });
54
- },
55
- close() {
56
- stopWaiting?.();
57
- popup.close();
58
- cancel?.();
59
- }
60
- };
61
- } };
120
+ finish(Effect.succeed({ callbackUrl: candidate }));
121
+ close();
122
+ } catch {}
123
+ };
124
+ const message = (event) => {
125
+ if (event.origin === expected.origin && event.source === popup && isCallbackMessage(event.data)) accept(event.data.callbackUrl);
126
+ };
127
+ const cleanup = () => {
128
+ window.clearInterval(timer);
129
+ window.removeEventListener("message", message);
130
+ cancel = void 0;
131
+ };
132
+ cancel = () => finish(Effect.fail(new BrowserAuthenticationError({
133
+ reason: "Cancelled",
134
+ message: "Browser authentication was cancelled."
135
+ })));
136
+ window.addEventListener("message", message);
137
+ const timer = window.setInterval(() => {
138
+ if (popup.closed) {
139
+ cancel?.();
140
+ return;
141
+ }
142
+ try {
143
+ accept(popup.location.href);
144
+ } catch {}
145
+ }, 250);
146
+ try {
147
+ popup.location.assign(authorizationUrl);
148
+ } catch (cause) {
149
+ finish(Effect.fail(new BrowserAuthenticationError({
150
+ reason: "Platform",
151
+ message: "Could not open browser authorization.",
152
+ cause
153
+ })));
154
+ }
155
+ return Effect.sync(cleanup);
156
+ }).pipe(Effect.onInterrupt(() => Effect.sync(close)));
157
+ }) };
158
+ }) };
62
159
  }
63
160
  //#endregion
64
- export { createWebBrowserAuthentication };
161
+ export { createWebBrowserAuthentication, relayWebBrowserAuthenticationCallback };
65
162
 
66
163
  //# sourceMappingURL=createWebBrowserAuthentication.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"createWebBrowserAuthentication.js","names":[],"sources":["../src/createWebBrowserAuthentication.ts"],"sourcesContent":["import {\n BrowserAuthenticationCancelledError,\n type BrowserAuthentication,\n type BrowserAuthenticationSession,\n} from \"@party-stack/runtime\";\n\nfunction matchesRedirectUrl(candidate: string, redirectUrl: string): boolean {\n const actual = new URL(candidate);\n const expected = new URL(redirectUrl);\n return actual.origin === expected.origin && actual.pathname === expected.pathname;\n}\n\nfunction createRedirectSession(): BrowserAuthenticationSession {\n return {\n open(authorizationUrl) {\n location.assign(authorizationUrl);\n return new Promise(() => undefined);\n },\n close() {\n window.stop();\n },\n };\n}\n\nexport function createWebBrowserAuthentication(\n options: {\n popupTarget?: string;\n popupFeatures?: string;\n } = {}\n): BrowserAuthentication {\n return {\n start({ redirectUrl, presentation }) {\n if (presentation !== \"popup\") {\n return createRedirectSession();\n }\n const popup = window.open(\n \"about:blank\",\n options.popupTarget ?? \"party-stack-authentication\",\n options.popupFeatures ?? \"popup=yes,width=500,height=700\"\n );\n if (!popup) return createRedirectSession();\n\n let stopWaiting: (() => void) | undefined;\n let cancel: (() => void) | undefined;\n return {\n open(authorizationUrl) {\n popup.location.assign(authorizationUrl);\n return new Promise<{\n callbackUrl: string;\n }>((resolve, reject) => {\n cancel = () => {\n stopWaiting?.();\n cancel = undefined;\n reject(new BrowserAuthenticationCancelledError());\n };\n const timer = window.setInterval(() => {\n if (popup.closed) {\n cancel?.();\n return;\n }\n try {\n const current = popup.location.href;\n if (!matchesRedirectUrl(current, redirectUrl)) {\n return;\n }\n stopWaiting?.();\n cancel = undefined;\n popup.close();\n resolve({\n callbackUrl: current,\n });\n } catch {\n // Cross-origin access is expected until the provider returns.\n }\n }, 250);\n stopWaiting = () => {\n window.clearInterval(timer);\n stopWaiting = undefined;\n };\n });\n },\n close() {\n stopWaiting?.();\n popup.close();\n cancel?.();\n },\n };\n },\n };\n}\n"],"mappings":";;AAMA,SAAS,mBAAmB,WAAmB,aAA8B;CACzE,MAAM,SAAS,IAAI,IAAI,SAAS;CAChC,MAAM,WAAW,IAAI,IAAI,WAAW;CACpC,OAAO,OAAO,WAAW,SAAS,UAAU,OAAO,aAAa,SAAS;AAC7E;AAEA,SAAS,wBAAsD;CAC3D,OAAO;EACH,KAAK,kBAAkB;GACnB,SAAS,OAAO,gBAAgB;GAChC,OAAO,IAAI,cAAc,KAAA,CAAS;EACtC;EACA,QAAQ;GACJ,OAAO,KAAK;EAChB;CACJ;AACJ;AAEA,SAAgB,+BACZ,UAGI,CAAC,GACgB;CACrB,OAAO,EACH,MAAM,EAAE,aAAa,gBAAgB;EACjC,IAAI,iBAAiB,SACjB,OAAO,sBAAsB;EAEjC,MAAM,QAAQ,OAAO,KACjB,eACA,QAAQ,eAAe,8BACvB,QAAQ,iBAAiB,gCAC7B;EACA,IAAI,CAAC,OAAO,OAAO,sBAAsB;EAEzC,IAAI;EACJ,IAAI;EACJ,OAAO;GACH,KAAK,kBAAkB;IACnB,MAAM,SAAS,OAAO,gBAAgB;IACtC,OAAO,IAAI,SAEP,SAAS,WAAW;KACpB,eAAe;MACX,cAAc;MACd,SAAS,KAAA;MACT,OAAO,IAAI,oCAAoC,CAAC;KACpD;KACA,MAAM,QAAQ,OAAO,kBAAkB;MACnC,IAAI,MAAM,QAAQ;OACd,SAAS;OACT;MACJ;MACA,IAAI;OACA,MAAM,UAAU,MAAM,SAAS;OAC/B,IAAI,CAAC,mBAAmB,SAAS,WAAW,GACxC;OAEJ,cAAc;OACd,SAAS,KAAA;OACT,MAAM,MAAM;OACZ,QAAQ,EACJ,aAAa,QACjB,CAAC;MACL,QAAQ,CAER;KACJ,GAAG,GAAG;KACN,oBAAoB;MAChB,OAAO,cAAc,KAAK;MAC1B,cAAc,KAAA;KAClB;IACJ,CAAC;GACL;GACA,QAAQ;IACJ,cAAc;IACd,MAAM,MAAM;IACZ,SAAS;GACb;EACJ;CACJ,EACJ;AACJ"}
1
+ {"version":3,"file":"createWebBrowserAuthentication.js","names":[],"sources":["../src/createWebBrowserAuthentication.ts"],"sourcesContent":["import {\n BrowserAuthenticationError,\n type BrowserAuthentication,\n type BrowserAuthenticationSession,\n} from \"@party-stack/runtime\";\nimport { Effect, Schema } from \"effect\";\n\nconst callbackMessage = Schema.Struct({\n type: Schema.Literal(\"party-stack:browser-authentication:callback\"),\n callbackUrl: Schema.String,\n});\nconst isCallbackMessage = Schema.is(callbackMessage);\n\nconst acceptedMessage = Schema.Struct({\n type: Schema.Literal(\"party-stack:browser-authentication:accepted\"),\n state: Schema.NullOr(Schema.String),\n});\nconst isAcceptedMessage = Schema.is(acceptedMessage);\n\n/** Call on the redirect page. A different opener origin must be explicitly configured;\n * for example, a registered localhost callback returning to a named dev worktree.\n */\nexport function relayWebBrowserAuthenticationCallback(\n callbackUrl = window.location.href,\n options: { openerOrigin?: string; onAccepted?: () => void } = {}\n): (() => void) | undefined {\n const opener = window.opener;\n if (!opener) return;\n let cleanup = () => {};\n try {\n const callback = new URL(callbackUrl);\n if (callback.origin !== window.location.origin) return;\n const openerOrigin = new URL(options.openerOrigin ?? callback.origin).origin;\n const state = callback.searchParams.get(\"state\");\n const accepted = (event: MessageEvent<unknown>) => {\n if (\n event.origin !== openerOrigin ||\n event.source !== opener ||\n !isAcceptedMessage(event.data) ||\n event.data.state !== state\n )\n return;\n cleanup();\n options.onAccepted?.();\n };\n cleanup = () => window.removeEventListener(\"message\", accepted);\n window.addEventListener(\"message\", accepted);\n opener.postMessage(\n { type: \"party-stack:browser-authentication:callback\", callbackUrl },\n openerOrigin\n );\n return cleanup;\n } catch {\n cleanup();\n return;\n }\n}\n\nfunction matchesRedirectUrl(candidate: string, redirectUrl: string): boolean {\n const actual = new URL(candidate);\n const expected = new URL(redirectUrl);\n return actual.origin === expected.origin && actual.pathname === expected.pathname;\n}\n\n/** The acquisition runs synchronously on web: reserve before any OAuth/RPC await. */\nexport function createWebBrowserAuthentication(\n options: { popupTarget?: string; popupFeatures?: string } = {}\n): BrowserAuthentication {\n return {\n start: Effect.fnUntraced(function* ({ redirectUrl, presentation }) {\n const popup = yield* Effect.try({\n try: () => {\n new URL(redirectUrl);\n return presentation === \"popup\"\n ? window.open(\n \"about:blank\",\n options.popupTarget ?? \"party-stack-authentication\",\n options.popupFeatures ?? \"popup=yes,width=500,height=700\"\n )\n : null;\n },\n catch: (cause) =>\n new BrowserAuthenticationError({\n reason: \"Configuration\",\n message: \"Could not reserve browser authentication.\",\n cause,\n }),\n });\n if (presentation === \"popup\" && !popup)\n return yield* new BrowserAuthenticationError({\n reason: \"Platform\",\n message: \"The authentication popup was blocked.\",\n });\n let closed = false;\n let used = false;\n let cancel: (() => void) | undefined;\n const close = () => {\n if (closed) return;\n closed = true;\n cancel?.();\n if (popup) popup.close();\n else if (used) window.stop();\n };\n yield* Effect.addFinalizer(() => Effect.sync(close));\n return {\n open: (authorizationUrl: string) =>\n Effect.suspend(() => {\n if (used || closed)\n return Effect.fail(\n new BrowserAuthenticationError({\n reason: \"Configuration\",\n message: \"This authentication session has already been used or closed.\",\n })\n );\n used = true;\n if (!popup)\n return Effect.try({\n try: () => location.assign(authorizationUrl),\n catch: (cause) =>\n new BrowserAuthenticationError({\n reason: \"Platform\",\n message: \"Could not open browser authorization.\",\n cause,\n }),\n }).pipe(Effect.andThen(Effect.never));\n return Effect.callback<{ callbackUrl: string }, BrowserAuthenticationError>(\n (resume) => {\n const expected = new URL(redirectUrl);\n let state: string | null;\n try {\n state = new URL(authorizationUrl).searchParams.get(\"state\");\n } catch (cause) {\n resume(\n Effect.fail(\n new BrowserAuthenticationError({\n reason: \"Configuration\",\n message: \"Invalid authorization URL.\",\n cause,\n })\n )\n );\n return;\n }\n let done = false;\n const finish = (\n result: Effect.Effect<{ callbackUrl: string }, BrowserAuthenticationError>\n ) => {\n if (done) return;\n done = true;\n cleanup();\n resume(result);\n };\n const accept = (candidate: string) => {\n try {\n if (\n !matchesRedirectUrl(candidate, redirectUrl) ||\n new URL(candidate).searchParams.get(\"state\") !== state\n )\n return;\n try {\n popup.postMessage?.(\n {\n type: \"party-stack:browser-authentication:accepted\",\n state,\n },\n expected.origin\n );\n } catch {\n /* The callback is already verified. */\n }\n finish(Effect.succeed({ callbackUrl: candidate }));\n close();\n } catch {\n /* Ignore unrelated or malformed callback messages. */\n }\n };\n const message = (event: MessageEvent<unknown>) => {\n if (\n event.origin === expected.origin &&\n event.source === popup &&\n isCallbackMessage(event.data)\n )\n accept(event.data.callbackUrl);\n };\n const cleanup = () => {\n window.clearInterval(timer);\n window.removeEventListener(\"message\", message);\n cancel = undefined;\n };\n cancel = () =>\n finish(\n Effect.fail(\n new BrowserAuthenticationError({\n reason: \"Cancelled\",\n message: \"Browser authentication was cancelled.\",\n })\n )\n );\n window.addEventListener(\"message\", message);\n const timer = window.setInterval(() => {\n if (popup.closed) {\n cancel?.();\n return;\n }\n try {\n accept(popup.location.href);\n } catch {\n /* A cross-origin popup is expected. */\n }\n }, 250);\n try {\n popup.location.assign(authorizationUrl);\n } catch (cause) {\n finish(\n Effect.fail(\n new BrowserAuthenticationError({\n reason: \"Platform\",\n message: \"Could not open browser authorization.\",\n cause,\n })\n )\n );\n }\n return Effect.sync(cleanup);\n }\n ).pipe(Effect.onInterrupt(() => Effect.sync(close)));\n }),\n } satisfies BrowserAuthenticationSession;\n }),\n };\n}\n"],"mappings":"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"}
package/lib/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- import { createWebRuntime } from "./createWebRuntime.js";
2
- export { createWebRuntime };
1
+ import { WebRuntimeOptions, layer, make, webRuntime } from "./WebRuntime.js";
2
+ import { createWebBrowserAuthentication, relayWebBrowserAuthenticationCallback } from "./createWebBrowserAuthentication.js";
3
+ export { type WebRuntimeOptions, createWebBrowserAuthentication, layer, make, relayWebBrowserAuthenticationCallback, webRuntime };
package/lib/index.js CHANGED
@@ -1 +1,3 @@
1
- export { createWebRuntime } from "./createWebRuntime.js";
1
+ import { createWebBrowserAuthentication, relayWebBrowserAuthenticationCallback } from "./createWebBrowserAuthentication.js";
2
+ import { layer, make, webRuntime } from "./WebRuntime.js";
3
+ export { createWebBrowserAuthentication, layer, make, relayWebBrowserAuthenticationCallback, webRuntime };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@party-stack/web-runtime",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "files": [
5
5
  "lib"
6
6
  ],
@@ -12,19 +12,25 @@
12
12
  }
13
13
  },
14
14
  "dependencies": {
15
- "effection": "^4.0.2",
16
- "@party-stack/coordination": "0.3.1",
17
- "@party-stack/db-indexeddb-persistence": "0.3.0",
18
- "@party-stack/runtime": "0.4.2"
15
+ "@effect/platform-browser": "^4.0.5",
16
+ "effect": "^4.0.5",
17
+ "@party-stack/indexeddb-ontology-storage": "0.2.0",
18
+ "@party-stack/ontology-storage": "0.2.0",
19
+ "@party-stack/runtime": "0.5.0"
19
20
  },
20
21
  "devDependencies": {
21
- "@tanstack/db": "0.14.0",
22
- "@tanstack/db-sqlite-persistence-core": "0.4.9",
22
+ "@tanstack/db": "1.0.0-rc.1.4",
23
23
  "playwright": "^1.61.1",
24
24
  "typescript": "~7.0.2",
25
25
  "vite": "npm:@voidzero-dev/vite-plus-core@1.2.0",
26
26
  "vite-plus": "1.2.0",
27
- "@party-stack/build-config": "0.1.3"
27
+ "@party-stack/build-config": "0.2.0",
28
+ "@party-stack/connect": "0.2.0",
29
+ "@party-stack/ontology-backend": "0.2.0",
30
+ "@party-stack/ontology": "0.21.0",
31
+ "@party-stack/ontology-replica": "0.2.0",
32
+ "@party-stack/ontology-server": "0.11.0",
33
+ "@party-stack/ontology-spec": "0.2.0"
28
34
  },
29
35
  "scripts": {
30
36
  "test:browser": "tsc -p tsconfig.e2e.json && vp test run --config vitest.e2e.config.mjs"
@@ -1,5 +0,0 @@
1
- import { RuntimeAdapter } from "@party-stack/runtime";
2
- //#region src/createWebRuntime.d.ts
3
- export declare const createWebRuntime: (owner: string, namespace: string) => RuntimeAdapter;
4
- //#endregion
5
- //# sourceMappingURL=createWebRuntime.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"createWebRuntime.d.ts","names":[],"sources":["../src/createWebRuntime.ts"],"mappings":";;qBAuBa,mBAAmB,eAAe,sBAAsB"}
@@ -1,43 +0,0 @@
1
- import { NavigatorNetworkConnectivity } from "./NavigatorNetworkConnectivity.js";
2
- import { OPFSBlobBytesStore } from "./OPFSBlobBytesStore.js";
3
- import { createWebBrowserAuthentication } from "./createWebBrowserAuthentication.js";
4
- import { defineRuntime } from "@party-stack/runtime";
5
- import { LockBroadcastCoordination } from "@party-stack/coordination";
6
- import { createIndexedDBPersistence } from "@party-stack/db-indexeddb-persistence";
7
- //#region src/createWebRuntime.ts
8
- function deleteIndexedDB(name) {
9
- return new Promise((resolve, reject) => {
10
- const request = indexedDB.deleteDatabase(name);
11
- request.onsuccess = () => resolve();
12
- request.onerror = () => reject(new Error(`IndexedDB database "${name}" could not be deleted.`, { cause: request.error }));
13
- request.onblocked = () => reject(/* @__PURE__ */ new Error(`IndexedDB database "${name}" could not be deleted because it is open.`));
14
- });
15
- }
16
- const createWebRuntime = defineRuntime((owner, namespace) => {
17
- const name = `party-stack:${owner}:${namespace}`;
18
- const persistence = createIndexedDBPersistence({ databaseName: name });
19
- const connectivity = NavigatorNetworkConnectivity.create();
20
- const coordination = new LockBroadcastCoordination({ scope: name });
21
- const blobBytes = new OPFSBlobBytesStore({ directoryName: `${name}:blobs` });
22
- return {
23
- owner,
24
- namespace,
25
- blobBytes,
26
- coordination,
27
- connectivity,
28
- browserAuthentication: createWebBrowserAuthentication(),
29
- persistence,
30
- destroy: async () => {
31
- await Promise.all([deleteIndexedDB(name), blobBytes.clear()]);
32
- },
33
- cleanup: async () => {
34
- await coordination.close();
35
- await connectivity.close();
36
- persistence.close();
37
- }
38
- };
39
- });
40
- //#endregion
41
- export { createWebRuntime };
42
-
43
- //# sourceMappingURL=createWebRuntime.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"createWebRuntime.js","names":[],"sources":["../src/createWebRuntime.ts"],"sourcesContent":["import { LockBroadcastCoordination } from \"@party-stack/coordination\";\nimport { createIndexedDBPersistence } from \"@party-stack/db-indexeddb-persistence\";\nimport { defineRuntime } from \"@party-stack/runtime\";\nimport { createWebBrowserAuthentication } from \"./createWebBrowserAuthentication.js\";\nimport { NavigatorNetworkConnectivity } from \"./NavigatorNetworkConnectivity.js\";\nimport { OPFSBlobBytesStore } from \"./OPFSBlobBytesStore.js\";\nimport type { RuntimeAdapter } from \"@party-stack/runtime\";\n\nfunction deleteIndexedDB(name: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const request = indexedDB.deleteDatabase(name);\n request.onsuccess = () => resolve();\n request.onerror = () =>\n reject(\n new Error(`IndexedDB database \"${name}\" could not be deleted.`, {\n cause: request.error,\n })\n );\n request.onblocked = () =>\n reject(new Error(`IndexedDB database \"${name}\" could not be deleted because it is open.`));\n });\n}\n\nexport const createWebRuntime: (owner: string, namespace: string) => RuntimeAdapter = defineRuntime(\n (owner, namespace) => {\n const name = `party-stack:${owner}:${namespace}`;\n const persistence = createIndexedDBPersistence({\n databaseName: name,\n });\n const connectivity = NavigatorNetworkConnectivity.create();\n const coordination = new LockBroadcastCoordination({\n scope: name,\n });\n const blobBytes = new OPFSBlobBytesStore({\n directoryName: `${name}:blobs`,\n });\n return {\n owner,\n namespace,\n blobBytes,\n coordination,\n connectivity,\n browserAuthentication: createWebBrowserAuthentication(),\n persistence,\n destroy: async () => {\n await Promise.all([deleteIndexedDB(name), blobBytes.clear()]);\n },\n cleanup: async () => {\n await coordination.close();\n await connectivity.close();\n persistence.close();\n },\n };\n }\n);\n"],"mappings":";;;;;;;AAQA,SAAS,gBAAgB,MAA6B;CAClD,OAAO,IAAI,SAAS,SAAS,WAAW;EACpC,MAAM,UAAU,UAAU,eAAe,IAAI;EAC7C,QAAQ,kBAAkB,QAAQ;EAClC,QAAQ,gBACJ,OACI,IAAI,MAAM,uBAAuB,KAAK,0BAA0B,EAC5D,OAAO,QAAQ,MACnB,CAAC,CACL;EACJ,QAAQ,kBACJ,uBAAO,IAAI,MAAM,uBAAuB,KAAK,2CAA2C,CAAC;CACjG,CAAC;AACL;AAEA,MAAa,mBAAyE,eACjF,OAAO,cAAc;CAClB,MAAM,OAAO,eAAe,MAAM,GAAG;CACrC,MAAM,cAAc,2BAA2B,EAC3C,cAAc,KAClB,CAAC;CACD,MAAM,eAAe,6BAA6B,OAAO;CACzD,MAAM,eAAe,IAAI,0BAA0B,EAC/C,OAAO,KACX,CAAC;CACD,MAAM,YAAY,IAAI,mBAAmB,EACrC,eAAe,GAAG,KAAK,QAC3B,CAAC;CACD,OAAO;EACH;EACA;EACA;EACA;EACA;EACA,uBAAuB,+BAA+B;EACtD;EACA,SAAS,YAAY;GACjB,MAAM,QAAQ,IAAI,CAAC,gBAAgB,IAAI,GAAG,UAAU,MAAM,CAAC,CAAC;EAChE;EACA,SAAS,YAAY;GACjB,MAAM,aAAa,MAAM;GACzB,MAAM,aAAa,MAAM;GACzB,YAAY,MAAM;EACtB;CACJ;AACJ,CACJ"}