@party-stack/web-runtime 0.4.1 → 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 +60 -0
- package/lib/NavigatorNetworkConnectivity.d.ts +6 -13
- package/lib/NavigatorNetworkConnectivity.d.ts.map +1 -1
- package/lib/NavigatorNetworkConnectivity.js +39 -60
- package/lib/NavigatorNetworkConnectivity.js.map +1 -1
- package/lib/OPFSBlobBytesStore.d.ts +7 -6
- package/lib/OPFSBlobBytesStore.d.ts.map +1 -1
- package/lib/OPFSBlobBytesStore.js +29 -15
- package/lib/OPFSBlobBytesStore.js.map +1 -1
- package/lib/WebRuntime.d.ts +30 -0
- package/lib/WebRuntime.d.ts.map +1 -0
- package/lib/WebRuntime.js +83 -0
- package/lib/WebRuntime.js.map +1 -0
- package/lib/createWebBrowserAuthentication.d.ts +8 -0
- package/lib/createWebBrowserAuthentication.d.ts.map +1 -1
- package/lib/createWebBrowserAuthentication.js +149 -52
- package/lib/createWebBrowserAuthentication.js.map +1 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.js +3 -2
- package/package.json +16 -10
- package/lib/createWebRuntime.d.ts +0 -5
- package/lib/createWebRuntime.d.ts.map +0 -1
- package/lib/createWebRuntime.js +0 -43
- package/lib/createWebRuntime.js.map +0 -1
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 {
|
|
1
|
+
import { Effect, Stream } from "effect";
|
|
2
2
|
//#region src/NavigatorNetworkConnectivity.d.ts
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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":"
|
|
1
|
+
{"version":3,"file":"NavigatorNetworkConnectivity.d.ts","names":[],"sources":["../src/NavigatorNetworkConnectivity.ts"],"mappings":";;;qBAMa,wCAAgC,OAAA;;;iCA+C3C"}
|
|
@@ -1,64 +1,43 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { makeNetworkConnectivity } from "@party-stack/runtime";
|
|
2
|
+
import { Effect, PubSub, Stream } from "effect";
|
|
2
3
|
//#region src/NavigatorNetworkConnectivity.ts
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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":"
|
|
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
|
-
|
|
3
|
-
|
|
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,
|
|
11
|
-
const writable = await (await (await this.#directory()).getFileHandle(
|
|
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
|
|
14
|
-
}
|
|
15
|
-
|
|
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
|
|
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(
|
|
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
|
|
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":"
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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 {
|
|
2
|
-
|
|
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,2 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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.
|
|
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
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"@party-stack/
|
|
18
|
-
"@party-stack/
|
|
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.
|
|
22
|
-
"@tanstack/db-sqlite-persistence-core": "0.4.5",
|
|
22
|
+
"@tanstack/db": "1.0.0-rc.1.4",
|
|
23
23
|
"playwright": "^1.61.1",
|
|
24
24
|
"typescript": "~7.0.2",
|
|
25
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@1.
|
|
26
|
-
"vite-plus": "1.
|
|
27
|
-
"@party-stack/build-config": "0.
|
|
25
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@1.2.0",
|
|
26
|
+
"vite-plus": "1.2.0",
|
|
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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"createWebRuntime.d.ts","names":[],"sources":["../src/createWebRuntime.ts"],"mappings":";;qBAuBa,mBAAmB,eAAe,sBAAsB"}
|
package/lib/createWebRuntime.js
DELETED
|
@@ -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"}
|