Compare commits

..

22 Commits

Author SHA1 Message Date
Anselm
b09e35e372 release 2024-07-29 10:40:10 +01:00
Anselm
d2c8121c9c Fix storage option in jazz-react 2024-07-29 10:34:39 +01:00
Anselm
380bb88ffa Mostly complete OPFS implementation (single-tab only) 2024-07-29 10:33:18 +01:00
Anselm
6ab53c263d Release 2024-07-26 17:23:02 +01:00
Anselm
e7f3e4e242 Increase disconnect timeout for now 2024-07-26 17:21:06 +01:00
Anselm Eickhoff
8bb5201647 Merge pull request #236 from timolins/patch-1
[Homepage] Make current year dynamic in footer
2024-07-22 15:55:16 +01:00
Timo Lins
a9fc94f53d Make current year dynamic in footer 2024-07-22 10:06:48 +02:00
Anselm Eickhoff
ca7c0510d1 Merge pull request #234 from Schniz/schniz/co-optional 2024-07-21 10:14:41 +01:00
Gal Schlezinger
1bf16f0859 add co.optional syntax 2024-07-21 09:10:44 +03:00
Anselm Eickhoff
21b503c188 Merge pull request #224 from datner/datner/give-it-a-try
cojson-transport-ws: reuse runtime and use fibers instead of setTimeout
2024-07-15 14:35:18 +01:00
Anselm
0053e9796c Release 2024-07-15 11:01:31 +01:00
Anselm
e84941b1b1 Fix another bug in CoMap 'has' proxy trap 2024-07-15 10:59:14 +01:00
Anselm
57f6f8d67e Release 2024-07-14 17:55:47 +01:00
Anselm
5b8e69d973 Fix bug in CoMap 'has' trap 2024-07-14 17:55:05 +01:00
Yuval Datner
8c8f85859c style: prettier 2024-07-12 15:42:52 +03:00
Yuval Datner
104384409e refactor: change to yieldable error 2024-07-12 15:42:27 +03:00
Yuval Datner
179827ae56 small refactor for readability 2024-07-12 15:13:25 +03:00
Yuval Datner
6645829876 do stream stuff 2024-07-12 15:13:23 +03:00
Gal Schlezinger
68cb302722 store jazzPings on global 2024-07-12 15:13:22 +03:00
Gal Schlezinger
8dc33f2790 fix bugs because I misindented things 2024-07-12 15:13:21 +03:00
Gal Schlezinger
5f64ba326c jazzPings 2024-07-12 15:13:20 +03:00
Gal Schlezinger
7ccb15107c cojson-transport-ws: reuse runtime and use fibers instead of setTimeout
when calling Effect.runFork we don't propagate layers
and using fibers can allow us to interrupt ongoing requests
when the pings fail
2024-07-12 15:13:16 +03:00
47 changed files with 834 additions and 239 deletions

View File

@@ -1,5 +1,36 @@
# jazz-example-chat # jazz-example-chat
## 0.0.70
### Patch Changes
- Updated dependencies
- cojson@0.7.23
- jazz-react@0.7.23
- jazz-tools@0.7.23
## 0.0.69
### Patch Changes
- jazz-react@0.7.22
## 0.0.68
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.21
- jazz-react@0.7.21
## 0.0.67
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.20
- jazz-react@0.7.20
## 0.0.66 ## 0.0.66
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "jazz-example-chat", "name": "jazz-example-chat",
"private": true, "private": true,
"version": "0.0.66", "version": "0.0.70",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -1,5 +1,20 @@
# jazz-example-chat # jazz-example-chat
## 0.0.51
### Patch Changes
- Updated dependencies
- cojson@0.7.23
- cojson-transport-ws@0.7.23
## 0.0.50
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.22
## 0.0.49 ## 0.0.49
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "jazz-inspector", "name": "jazz-inspector",
"private": true, "private": true,
"version": "0.0.49", "version": "0.0.51",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -1,5 +1,39 @@
# jazz-example-pets # jazz-example-pets
## 0.0.88
### Patch Changes
- Updated dependencies
- jazz-react@0.7.23
- jazz-tools@0.7.23
- jazz-browser-media-images@0.7.23
## 0.0.87
### Patch Changes
- jazz-browser-media-images@0.7.22
- jazz-react@0.7.22
## 0.0.86
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.21
- jazz-browser-media-images@0.7.21
- jazz-react@0.7.21
## 0.0.85
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.20
- jazz-browser-media-images@0.7.20
- jazz-react@0.7.20
## 0.0.84 ## 0.0.84
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "jazz-example-pets", "name": "jazz-example-pets",
"private": true, "private": true,
"version": "0.0.84", "version": "0.0.88",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -1,5 +1,35 @@
# jazz-example-todo # jazz-example-todo
## 0.0.87
### Patch Changes
- Updated dependencies
- jazz-react@0.7.23
- jazz-tools@0.7.23
## 0.0.86
### Patch Changes
- jazz-react@0.7.22
## 0.0.85
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.21
- jazz-react@0.7.21
## 0.0.84
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.20
- jazz-react@0.7.20
## 0.0.83 ## 0.0.83
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "jazz-example-todo", "name": "jazz-example-todo",
"private": true, "private": true,
"version": "0.0.83", "version": "0.0.87",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -11,8 +11,8 @@ import { SiGithub, SiDiscord, SiTwitter } from "@icons-pack/react-simple-icons";
import { Nav, NavLink, Newsletter, NewsletterButton } from "@/components/nav"; import { Nav, NavLink, Newsletter, NewsletterButton } from "@/components/nav";
import { DocNav } from "@/components/docs/nav"; import { DocNav } from "@/components/docs/nav";
import { SpeedInsights } from "@vercel/speed-insights/next" import { SpeedInsights } from "@vercel/speed-insights/next";
import { Analytics } from "@vercel/analytics/react" import { Analytics } from "@vercel/analytics/react";
// If loading a variable font, you don't need to specify the font weight // If loading a variable font, you don't need to specify the font weight
const manrope = Manrope({ const manrope = Manrope({
@@ -50,8 +50,8 @@ export default function RootLayout({
"flex flex-col items-center bg-stone-50 dark:bg-stone-950 overflow-x-hidden", "flex flex-col items-center bg-stone-50 dark:bg-stone-950 overflow-x-hidden",
].join(" ")} ].join(" ")}
> >
<SpeedInsights/> <SpeedInsights />
<Analytics/> <Analytics />
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"
defaultTheme="system" defaultTheme="system"
@@ -112,7 +112,7 @@ export default function RootLayout({
<div className="col-span-full md:col-span-1 sm:row-start-4 md:row-start-auto lg:col-span-2 md:row-span-2 md:flex-1 flex flex-row md:flex-col max-sm:mt-4 justify-between max-sm:items-start gap-2 text-sm min-w-[10rem]"> <div className="col-span-full md:col-span-1 sm:row-start-4 md:row-start-auto lg:col-span-2 md:row-span-2 md:flex-1 flex flex-row md:flex-col max-sm:mt-4 justify-between max-sm:items-start gap-2 text-sm min-w-[10rem]">
<GcmpLogo monochrome className="w-32" /> <GcmpLogo monochrome className="w-32" />
<p className="max-sm:text-right"> <p className="max-sm:text-right">
© 2023 © {new Date().getFullYear()}
<br /> <br />
Garden Computing, Inc. Garden Computing, Inc.
</p> </p>

View File

@@ -83,4 +83,4 @@ export function GcmpLogo({
</defs> </defs>
</svg> </svg>
); );
} }

View File

@@ -1,5 +1,12 @@
# cojson-storage-indexeddb # cojson-storage-indexeddb
## 0.7.23
### Patch Changes
- Updated dependencies
- cojson@0.7.23
## 0.7.18 ## 0.7.18
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "cojson-storage-indexeddb", "name": "cojson-storage-indexeddb",
"version": "0.7.18", "version": "0.7.23",
"main": "dist/index.js", "main": "dist/index.js",
"type": "module", "type": "module",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,5 +1,12 @@
# cojson-storage-sqlite # cojson-storage-sqlite
## 0.7.23
### Patch Changes
- Updated dependencies
- cojson@0.7.23
## 0.7.18 ## 0.7.18
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "cojson-storage-sqlite", "name": "cojson-storage-sqlite",
"type": "module", "type": "module",
"version": "0.7.18", "version": "0.7.23",
"main": "dist/index.js", "main": "dist/index.js",
"types": "src/index.ts", "types": "src/index.ts",
"license": "MIT", "license": "MIT",

View File

@@ -1,5 +1,18 @@
# cojson-transport-nodejs-ws # cojson-transport-nodejs-ws
## 0.7.23
### Patch Changes
- Updated dependencies
- cojson@0.7.23
## 0.7.22
### Patch Changes
- Increase disconnect timeout for now
## 0.7.18 ## 0.7.18
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "cojson-transport-ws", "name": "cojson-transport-ws",
"type": "module", "type": "module",
"version": "0.7.18", "version": "0.7.23",
"main": "dist/index.js", "main": "dist/index.js",
"types": "src/index.ts", "types": "src/index.ts",
"license": "MIT", "license": "MIT",

View File

@@ -1,20 +1,37 @@
import { DisconnectedError, Peer, PingTimeoutError, SyncMessage } from "cojson"; import { DisconnectedError, Peer, PingTimeoutError, SyncMessage } from "cojson";
import { Either, Stream, Queue, Effect, Exit } from "effect"; import { Stream, Queue, Effect, Console } from "effect";
interface WebsocketEvents {
close: { code: number; reason: string };
message: { data: unknown };
open: void;
}
interface PingMsg {
time: number;
dc: string;
}
interface AnyWebSocket { interface AnyWebSocket {
addEventListener( addEventListener<K extends keyof WebsocketEvents>(
type: "close", type: K,
listener: (event: { code: number; reason: string }) => void, listener: (event: WebsocketEvents[K]) => void,
): void; ): void;
addEventListener( removeEventListener<K extends keyof WebsocketEvents>(
type: "message", type: K,
listener: (event: { data: string | unknown }) => void, listener: (event: WebsocketEvents[K]) => void,
): void; ): void;
addEventListener(type: "open", listener: () => void): void;
close(): void; close(): void;
send(data: string): void; send(data: string): void;
} }
const g: typeof globalThis & {
jazzPings?: {
received: number;
sent: number;
dc: string;
}[];
} = globalThis;
export function createWebSocketPeer(options: { export function createWebSocketPeer(options: {
id: string; id: string;
websocket: AnyWebSocket; websocket: AnyWebSocket;
@@ -22,87 +39,81 @@ export function createWebSocketPeer(options: {
}): Effect.Effect<Peer> { }): Effect.Effect<Peer> {
return Effect.gen(function* () { return Effect.gen(function* () {
const ws = options.websocket; const ws = options.websocket;
const ws_ = ws as unknown as Stream.EventListener<WebsocketEvents["message"]>;
const incoming =
yield* Queue.unbounded<
Either.Either<SyncMessage, DisconnectedError | PingTimeoutError>
>();
const outgoing = yield* Queue.unbounded<SyncMessage>(); const outgoing = yield* Queue.unbounded<SyncMessage>();
ws.addEventListener("close", (event) => { const closed = once(ws, "close").pipe(
void Effect.runPromiseExit( Effect.flatMap(
Queue.offer( (event) =>
incoming, new DisconnectedError({
Either.left( message: `${event.code}: ${event.reason}`,
new DisconnectedError(`${event.code}: ${event.reason}`), }),
),
Stream.fromEffect,
);
const isSyncMessage = (msg: unknown): msg is SyncMessage => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((msg as any)?.type === "ping") {
const ping = msg as PingMsg;
g.jazzPings ||= [];
g.jazzPings.push({
received: Date.now(),
sent: ping.time,
dc: ping.dc,
});
return false;
}
return true;
};
yield* Effect.forkDaemon(Effect.gen(function* () {
yield* once(ws, "open");
yield* Queue.take(outgoing).pipe(
Effect.andThen((message) => ws.send(JSON.stringify(message))),
Effect.forever,
);
}));
type E = WebsocketEvents["message"];
const messages = Stream.fromEventListener<E>(ws_, "message").pipe(
Stream.timeoutFail(() => new PingTimeoutError(), "10 seconds"),
Stream.tapError((_e) =>
Console.warn("Ping timeout").pipe(
Effect.andThen(Effect.try(() => ws.close())),
Effect.catchAll((e) =>
Console.error(
"Error while trying to close ws on ping timeout",
e,
),
), ),
), ),
).then((e) => { ),
if (Exit.isFailure(e) && !Exit.isInterrupted(e)) { Stream.mergeLeft(closed),
console.warn("Failed closing ws", e); Stream.map((_) => JSON.parse(_.data as string)),
} Stream.filter(isSyncMessage),
}); Stream.buffer({ capacity: "unbounded" }),
}); Stream.onDone(() => Queue.shutdown(outgoing)),
);
let pingTimeout: ReturnType<typeof setTimeout> | undefined;
ws.addEventListener("message", (event) => {
const msg = JSON.parse(event.data as string);
if (pingTimeout) {
clearTimeout(pingTimeout);
}
pingTimeout = setTimeout(() => {
console.debug("Ping timeout");
void Effect.runPromise(
Queue.offer(incoming, Either.left(new PingTimeoutError())),
);
try {
ws.close();
} catch (e) {
console.error(
"Error while trying to close ws on ping timeout",
e,
);
}
}, 2500);
if (msg.type === "ping") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).jazzPings =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).jazzPings || [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).jazzPings.push({
received: Date.now(),
sent: msg.time,
dc: msg.dc,
});
return;
} else {
void Effect.runPromise(
Queue.offer(incoming, Either.right(msg)),
);
}
});
ws.addEventListener("open", () => {
void Stream.fromQueue(outgoing).pipe(
Stream.runForEach((msg) =>
Effect.sync(() => ws.send(JSON.stringify(msg))),
),
Effect.runPromise,
);
});
return { return {
id: options.id, id: options.id,
incoming: Stream.fromQueue(incoming, { shutdown: true }).pipe( incoming: messages,
Stream.mapEffect((either) => either),
),
outgoing, outgoing,
role: options.role, role: options.role,
}; };
}); });
} }
const once = <Event extends keyof WebsocketEvents>(
ws: AnyWebSocket,
event: Event,
) =>
Effect.async<WebsocketEvents[Event]>((register) => {
const cb = (msg: WebsocketEvents[Event]) => {
ws.removeEventListener(event, cb);
register(Effect.succeed(msg));
};
ws.addEventListener(event, cb);
});

View File

@@ -1,5 +1,11 @@
# cojson # cojson
## 0.7.23
### Patch Changes
- Mostly complete OPFS implementation (single-tab only)
## 0.7.18 ## 0.7.18
### Patch Changes ### Patch Changes

View File

@@ -5,7 +5,7 @@
"types": "src/index.ts", "types": "src/index.ts",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"version": "0.7.18", "version": "0.7.23",
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.3", "@types/jest": "^29.5.3",
"@typescript-eslint/eslint-plugin": "^6.2.1", "@typescript-eslint/eslint-plugin": "^6.2.1",

View File

@@ -3,7 +3,7 @@ import { CoValueChunk } from "./index.js";
import { RawCoID } from "../ids.js"; import { RawCoID } from "../ids.js";
import { CryptoProvider, StreamingHash } from "../crypto/crypto.js"; import { CryptoProvider, StreamingHash } from "../crypto/crypto.js";
export type BlockFilename = `${string}-L${number}-H${number}.jsonl`; export type BlockFilename = `L${number}-${string}-${string}-H${number}.jsonl`;
export type BlockHeader = { id: RawCoID; start: number; length: number }[]; export type BlockHeader = { id: RawCoID; start: number; length: number }[];
@@ -78,8 +78,9 @@ export function readHeader<RH, FS extends FileSystem<unknown, RH>>(
export function writeBlock<WH, RH, FS extends FileSystem<WH, RH>>( export function writeBlock<WH, RH, FS extends FileSystem<WH, RH>>(
chunks: Map<RawCoID, CoValueChunk>, chunks: Map<RawCoID, CoValueChunk>,
level: number, level: number,
blockNumber: number,
fs: FS, fs: FS,
): Effect.Effect<void, FSErr> { ): Effect.Effect<BlockFilename, FSErr> {
if (chunks.size === 0) { if (chunks.size === 0) {
return Effect.die(new Error("No chunks to write")); return Effect.die(new Error("No chunks to write"));
} }
@@ -125,12 +126,17 @@ export function writeBlock<WH, RH, FS extends FileSystem<WH, RH>>(
// ), // ),
// ); // );
const filename: BlockFilename = `${hash.digest()}-L${level}-H${ const filename: BlockFilename = `L${level}-${(
headerBytes.length blockNumber + ""
}.jsonl`; ).padStart(3, "0")}-${hash
.digest()
.replace("hash_", "")
.slice(0, 15)}-H${headerBytes.length}.jsonl`;
// console.log("renaming to" + filename); // console.log("renaming to" + filename);
yield* $(fs.closeAndRename(file, filename)); yield* $(fs.closeAndRename(file, filename));
return filename;
// console.log("Wrote block", filename, blockHeader); // console.log("Wrote block", filename, blockHeader);
// console.log("IDs in block", blockHeader.map(e => e.id)); // console.log("IDs in block", blockHeader.map(e => e.id));
}); });
@@ -148,6 +154,7 @@ export function writeToWal<WH, RH, FS extends FileSystem<WH, RH>>(
...chunk, ...chunk,
}; };
const bytes = textEncoder.encode(JSON.stringify(walEntry) + "\n"); const bytes = textEncoder.encode(JSON.stringify(walEntry) + "\n");
console.log("writing to WAL", handle, id, bytes.length);
yield* $(fs.append(handle, bytes)); yield* $(fs.append(handle, bytes));
}); });
} }

View File

@@ -1,4 +1,11 @@
import { Effect, Either, Queue, Stream, SynchronizedRef } from "effect"; import {
Effect,
Either,
Queue,
Stream,
SynchronizedRef,
Deferred,
} from "effect";
import { RawCoID } from "../ids.js"; import { RawCoID } from "../ids.js";
import { CoValueHeader, Transaction } from "../coValueCore.js"; import { CoValueHeader, Transaction } from "../coValueCore.js";
import { Signature } from "../crypto/crypto.js"; import { Signature } from "../crypto/crypto.js";
@@ -30,6 +37,8 @@ import {
} from "./FileSystem.js"; } from "./FileSystem.js";
export type { FSErr, BlockFilename, WalFilename } from "./FileSystem.js"; export type { FSErr, BlockFilename, WalFilename } from "./FileSystem.js";
const MAX_N_LEVELS = 3;
export type CoValueChunk = { export type CoValueChunk = {
header?: CoValueHeader; header?: CoValueHeader;
sessionEntries: { sessionEntries: {
@@ -51,6 +60,10 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
BlockFilename, BlockFilename,
{ [id: RawCoID]: { start: number; length: number } } { [id: RawCoID]: { start: number; length: number } }
>(); >();
blockFileHandles = new Map<
BlockFilename,
Deferred.Deferred<{ handle: RH; size: number }, FSErr>
>();
constructor( constructor(
public fs: FS, public fs: FS,
@@ -192,7 +205,7 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
let newWal = wal; let newWal = wal;
if (!newWal) { if (!newWal) {
newWal = yield* this.fs.createFile( newWal = yield* this.fs.createFile(
`wal-${new Date().toISOString()}-${Math.random() `wal-${Date.now()}-${Math.random()
.toString(36) .toString(36)
.slice(2)}.jsonl`, .slice(2)}.jsonl`,
); );
@@ -314,24 +327,63 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
); );
} }
loadCoValue<WH, RH, FS extends FileSystem<WH, RH>>( getBlockHandle(
blockFile: BlockFilename,
fs: FS,
): Effect.Effect<{ handle: RH; size: number }, FSErr> {
return Effect.gen(this, function* () {
let handleAndSize = this.blockFileHandles.get(blockFile);
if (!handleAndSize) {
handleAndSize = yield* Deferred.make<
{ handle: RH; size: number },
FSErr
>();
this.blockFileHandles.set(blockFile, handleAndSize);
yield* Deferred.complete(
handleAndSize,
fs.openToRead(blockFile),
);
}
return yield* Deferred.await(handleAndSize);
});
}
loadCoValue(
id: RawCoID, id: RawCoID,
fs: FS, fs: FS,
): Effect.Effect<CoValueChunk | undefined, FSErr> { ): Effect.Effect<CoValueChunk | undefined, FSErr> {
// return _loadChunkFromWal(id, fs);
return Effect.gen(this, function* () { return Effect.gen(this, function* () {
const files = this.fileCache || (yield* fs.listFiles()); const files = this.fileCache || (yield* fs.listFiles());
this.fileCache = files; this.fileCache = files;
const blockFiles = files.filter((name) => const blockFiles = (
name.startsWith("hash_"), files.filter((name) => name.startsWith("L")) as BlockFilename[]
) as BlockFilename[]; ).sort();
let result;
for (const blockFile of blockFiles) { for (const blockFile of blockFiles) {
let cachedHeader: let cachedHeader:
| { [id: RawCoID]: { start: number; length: number } } | { [id: RawCoID]: { start: number; length: number } }
| undefined = this.headerCache.get(blockFile); | undefined = this.headerCache.get(blockFile);
const { handle, size } = yield* fs.openToRead(blockFile); let handleAndSize = this.blockFileHandles.get(blockFile);
if (!handleAndSize) {
handleAndSize = yield* Deferred.make<
{ handle: RH; size: number },
FSErr
>();
this.blockFileHandles.set(blockFile, handleAndSize);
yield* Deferred.complete(
handleAndSize,
fs.openToRead(blockFile),
);
}
const { handle, size } = yield* this.getBlockHandle(
blockFile,
fs,
);
// console.log("Attempting to load", id, blockFile); // console.log("Attempting to load", id, blockFile);
@@ -356,17 +408,29 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
// console.log("Header entry", id, headerEntry); // console.log("Header entry", id, headerEntry);
let result;
if (headerEntry) { if (headerEntry) {
result = yield* readChunk(handle, headerEntry, fs); const nextChunk = yield* readChunk(handle, headerEntry, fs);
if (result) {
const merged = mergeChunks(result, nextChunk);
if (Either.isRight(merged)) {
yield* Effect.logWarning(
"Non-contigous chunks while loading " + id,
result,
nextChunk,
);
} else {
result = merged.left;
}
} else {
result = nextChunk;
}
} }
yield* fs.close(handle); // yield* fs.close(handle);
return result;
} }
return undefined; return result;
}); });
} }
@@ -434,11 +498,150 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
yield* this.fs.close(handle); yield* this.fs.close(handle);
} }
yield* writeBlock(coValues, 0, this.fs); const highestBlockNumber = fileNames.reduce((acc, name) => {
if (name.startsWith("L" + MAX_N_LEVELS)) {
const num = parseInt(name.split("-")[1]!);
if (num > acc) {
return num;
}
}
return acc;
}, 0);
console.log(
[...coValues.keys()],
fileNames,
highestBlockNumber,
);
yield* writeBlock(
coValues,
MAX_N_LEVELS,
highestBlockNumber + 1,
this.fs,
);
for (const walFile of walFiles) { for (const walFile of walFiles) {
yield* this.fs.removeFile(walFile); yield* this.fs.removeFile(walFile);
} }
this.fileCache = undefined; this.fileCache = undefined;
const fileNames2 = yield* this.fs.listFiles();
const blockFiles = (
fileNames2.filter((name) =>
name.startsWith("L"),
) as BlockFilename[]
).sort();
const blockFilesByLevelInOrder: {
[level: number]: BlockFilename[];
} = {};
for (const blockFile of blockFiles) {
const level = parseInt(blockFile.split("-")[0]!.slice(1));
if (!blockFilesByLevelInOrder[level]) {
blockFilesByLevelInOrder[level] = [];
}
blockFilesByLevelInOrder[level]!.push(blockFile);
}
console.log(blockFilesByLevelInOrder);
for (let level = MAX_N_LEVELS; level > 0; level--) {
const nBlocksDesired = Math.pow(2, level);
const blocksInLevel = blockFilesByLevelInOrder[level];
if (
blocksInLevel &&
blocksInLevel.length > nBlocksDesired
) {
yield* Effect.log("Compacting blocks in level", level, blocksInLevel);
const coValues = new Map<RawCoID, CoValueChunk>();
for (const blockFile of blocksInLevel) {
const {
handle,
size,
}: { handle: RH; size: number } =
yield* this.getBlockHandle(blockFile, this.fs);
if (size === 0) {
continue;
}
const header = yield* readHeader(
blockFile,
handle,
size,
this.fs,
);
for (const entry of header) {
const chunk = yield* readChunk(
handle,
entry,
this.fs,
);
const existingChunk = coValues.get(entry.id);
if (existingChunk) {
const merged = mergeChunks(
existingChunk,
chunk,
);
if (Either.isRight(merged)) {
yield* Effect.logWarning(
"Non-contigous chunks in " +
entry.id +
", " +
blockFile,
existingChunk,
chunk,
);
} else {
coValues.set(entry.id, merged.left);
}
} else {
coValues.set(entry.id, chunk);
}
}
}
let levelBelow = blockFilesByLevelInOrder[level - 1];
if (!levelBelow) {
levelBelow = [];
blockFilesByLevelInOrder[level - 1] = levelBelow;
}
const highestBlockNumberInLevelBelow =
levelBelow.reduce((acc, name) => {
const num = parseInt(name.split("-")[1]!);
if (num > acc) {
return num;
}
return acc;
}, 0);
const newBlockName = yield* writeBlock(
coValues,
level - 1,
highestBlockNumberInLevelBelow + 1,
this.fs,
);
levelBelow.push(newBlockName);
// delete blocks that went into this one
for (const blockFile of blocksInLevel) {
const handle = yield* this.getBlockHandle(
blockFile,
this.fs,
);
yield* this.fs.close(handle.handle);
yield* this.fs.removeFile(blockFile);
}
}
}
}), }),
); );

View File

@@ -47,18 +47,24 @@ export function newQueuePair(
const queue = yield* Queue.unbounded<SyncMessage>(); const queue = yield* Queue.unbounded<SyncMessage>();
if (options.traceAs) { if (options.traceAs) {
return [Stream.fromQueue(queue).pipe(Stream.tap((msg) => Console.debug( return [
options.traceAs, Stream.fromQueue(queue).pipe(
JSON.stringify( Stream.tap((msg) =>
msg, Console.debug(
(k, v) => options.traceAs,
k === "changes" || JSON.stringify(
k === "encryptedChanges" msg,
? v.slice(0, 20) + "..." (k, v) =>
: v, k === "changes" || k === "encryptedChanges"
2, ? v.slice(0, 20) + "..."
: v,
2,
),
),
),
), ),
))), queue]; queue,
];
} else { } else {
return [Stream.fromQueue(queue), queue]; return [Stream.fromQueue(queue), queue];
} }

View File

@@ -3,7 +3,7 @@ import { CoValueHeader, Transaction } from "./coValueCore.js";
import { CoValueCore } from "./coValueCore.js"; import { CoValueCore } from "./coValueCore.js";
import { LocalNode, newLoadingState } from "./localNode.js"; import { LocalNode, newLoadingState } from "./localNode.js";
import { RawCoID, SessionID } from "./ids.js"; import { RawCoID, SessionID } from "./ids.js";
import { Effect, Queue, Stream } from "effect"; import { Data, Effect, Queue, Stream } from "effect";
export type CoValueKnownState = { export type CoValueKnownState = {
id: RawCoID; id: RawCoID;
@@ -56,12 +56,9 @@ export type DoneMessage = {
export type PeerID = string; export type PeerID = string;
export class DisconnectedError extends Error { export class DisconnectedError extends Data.TaggedError("DisconnectedError")<{
readonly _tag = "DisconnectedError"; message: string;
constructor(public message: string) { }> {}
super(message);
}
}
export class PingTimeoutError extends Error { export class PingTimeoutError extends Error {
readonly _tag = "PingTimeoutError"; readonly _tag = "PingTimeoutError";

View File

@@ -53,13 +53,15 @@ test("Can create account with one node, and then load it on another", async () =
map.set("foo", "bar", "private"); map.set("foo", "bar", "private");
expect(map.get("foo")).toEqual("bar"); expect(map.get("foo")).toEqual("bar");
const [node1asPeer, node2asPeer] = await Effect.runPromise(connectedPeers("node1", "node2", { const [node1asPeer, node2asPeer] = await Effect.runPromise(
trace: true, connectedPeers("node1", "node2", {
peer1role: "server", trace: true,
peer2role: "client", peer1role: "server",
})); peer2role: "client",
}),
);
console.log("After connected peers") console.log("After connected peers");
node.syncManager.addPeer(node2asPeer); node.syncManager.addPeer(node2asPeer);

View File

@@ -1,5 +1,35 @@
# jazz-browser-media-images # jazz-browser-media-images
## 0.7.23
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.23
- jazz-browser@0.7.23
## 0.7.22
### Patch Changes
- jazz-browser@0.7.22
## 0.7.21
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.21
- jazz-browser@0.7.21
## 0.7.20
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.20
- jazz-browser@0.7.20
## 0.7.19 ## 0.7.19
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "jazz-browser-media-images", "name": "jazz-browser-media-images",
"version": "0.7.19", "version": "0.7.23",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,5 +1,36 @@
# jazz-browser # jazz-browser
## 0.7.23
### Patch Changes
- Updated dependencies
- cojson@0.7.23
- jazz-tools@0.7.23
- cojson-storage-indexeddb@0.7.23
- cojson-transport-ws@0.7.23
## 0.7.22
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.22
## 0.7.21
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.21
## 0.7.20
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.20
## 0.7.19 ## 0.7.19
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "jazz-browser", "name": "jazz-browser",
"version": "0.7.19", "version": "0.7.23",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -7,7 +7,7 @@ import {
} from "cojson"; } from "cojson";
import { Effect } from "effect"; import { Effect } from "effect";
export class OPFSFilesystem implements FileSystem<number, number> { export class OPFSFilesystem implements FileSystem<{id: number, filename: string}, {id: number, filename: string}> {
opfsWorker: Worker; opfsWorker: Worker;
callbacks: Map<number, (event: MessageEvent) => void> = new Map(); callbacks: Map<number, (event: MessageEvent) => void> = new Map();
nextRequestId = 0; nextRequestId = 0;
@@ -31,13 +31,13 @@ export class OPFSFilesystem implements FileSystem<number, number> {
listFiles(): Effect.Effect<string[], FSErr, never> { listFiles(): Effect.Effect<string[], FSErr, never> {
return Effect.async((cb) => { return Effect.async((cb) => {
const requestId = this.nextRequestId++; const requestId = this.nextRequestId++;
performance.mark("listFiles" + requestId); performance.mark("listFiles" + requestId + "_listFiles");
this.callbacks.set(requestId, (event) => { this.callbacks.set(requestId, (event) => {
performance.mark("listFilesEnd" + requestId); performance.mark("listFilesEnd" + requestId + "_listFiles");
performance.measure( performance.measure(
"listFiles" + requestId, "listFiles" + requestId + "_listFiles",
"listFiles" + requestId, "listFiles" + requestId + "_listFiles",
"listFilesEnd" + requestId, "listFilesEnd" + requestId + "_listFiles",
); );
cb(Effect.succeed(event.data.fileNames)); cb(Effect.succeed(event.data.fileNames));
}); });
@@ -47,22 +47,22 @@ export class OPFSFilesystem implements FileSystem<number, number> {
openToRead( openToRead(
filename: string, filename: string,
): Effect.Effect<{ handle: number; size: number }, FSErr, never> { ): Effect.Effect<{ handle: {id: number, filename: string}; size: number }, FSErr, never> {
return Effect.async((cb) => { return Effect.async((cb) => {
const requestId = this.nextRequestId++; const requestId = this.nextRequestId++;
performance.mark("openToRead" + requestId); performance.mark("openToRead" + "_" + filename);
this.callbacks.set(requestId, (event) => { this.callbacks.set(requestId, (event) => {
cb( cb(
Effect.succeed({ Effect.succeed({
handle: event.data.handle, handle: {id: event.data.handle, filename},
size: event.data.size, size: event.data.size,
}), }),
); );
performance.mark("openToReadEnd" + requestId); performance.mark("openToReadEnd" + "_" + filename);
performance.measure( performance.measure(
"openToRead" + requestId, "openToRead" + "_" + filename,
"openToRead" + requestId, "openToRead" + "_" + filename,
"openToReadEnd" + requestId, "openToReadEnd" + "_" + filename,
); );
}); });
this.opfsWorker.postMessage({ this.opfsWorker.postMessage({
@@ -73,18 +73,18 @@ export class OPFSFilesystem implements FileSystem<number, number> {
}); });
} }
createFile(filename: string): Effect.Effect<number, FSErr, never> { createFile(filename: string): Effect.Effect<{id: number, filename: string}, FSErr, never> {
return Effect.async((cb) => { return Effect.async((cb) => {
const requestId = this.nextRequestId++; const requestId = this.nextRequestId++;
performance.mark("createFile" + requestId); performance.mark("createFile" + "_" + filename);
this.callbacks.set(requestId, (event) => { this.callbacks.set(requestId, (event) => {
performance.mark("createFileEnd" + requestId); performance.mark("createFileEnd" + "_" + filename);
performance.measure( performance.measure(
"createFile" + requestId, "createFile" + "_" + filename,
"createFile" + requestId, "createFile" + "_" + filename,
"createFileEnd" + requestId, "createFileEnd" + "_" + filename,
); );
cb(Effect.succeed(event.data.handle)); cb(Effect.succeed({id: event.data.handle, filename}));
}); });
this.opfsWorker.postMessage({ this.opfsWorker.postMessage({
type: "createFile", type: "createFile",
@@ -96,18 +96,18 @@ export class OPFSFilesystem implements FileSystem<number, number> {
openToWrite( openToWrite(
filename: string, filename: string,
): Effect.Effect<FileSystemFileHandle, FSErr, never> { ): Effect.Effect<{id: number, filename: string}, FSErr, never> {
return Effect.async((cb) => { return Effect.async((cb) => {
const requestId = this.nextRequestId++; const requestId = this.nextRequestId++;
performance.mark("openToWrite" + requestId); performance.mark("openToWrite" + "_" + filename);
this.callbacks.set(requestId, (event) => { this.callbacks.set(requestId, (event) => {
performance.mark("openToWriteEnd" + requestId); performance.mark("openToWriteEnd" + "_" + filename);
performance.measure( performance.measure(
"openToWrite" + requestId, "openToWrite" + "_" + filename,
"openToWrite" + requestId, "openToWrite" + "_" + filename,
"openToWriteEnd" + requestId, "openToWriteEnd" + "_" + filename,
); );
cb(Effect.succeed(event.data.handle)); cb(Effect.succeed({id: event.data.handle, filename}));
}); });
this.opfsWorker.postMessage({ this.opfsWorker.postMessage({
type: "openToWrite", type: "openToWrite",
@@ -118,24 +118,24 @@ export class OPFSFilesystem implements FileSystem<number, number> {
} }
append( append(
handle: number, handle: {id: number, filename: string},
data: Uint8Array, data: Uint8Array,
): Effect.Effect<void, FSErr, never> { ): Effect.Effect<void, FSErr, never> {
return Effect.async((cb) => { return Effect.async((cb) => {
const requestId = this.nextRequestId++; const requestId = this.nextRequestId++;
performance.mark("append" + requestId); performance.mark("append" + "_" + handle.filename);
this.callbacks.set(requestId, (_) => { this.callbacks.set(requestId, (_) => {
performance.mark("appendEnd" + requestId); performance.mark("appendEnd" + "_" + handle.filename);
performance.measure( performance.measure(
"append" + requestId, "append" + "_" + handle.filename,
"append" + requestId, "append" + "_" + handle.filename,
"appendEnd" + requestId, "appendEnd" + "_" + handle.filename,
); );
cb(Effect.succeed(undefined)); cb(Effect.succeed(undefined));
}); });
this.opfsWorker.postMessage({ this.opfsWorker.postMessage({
type: "append", type: "append",
handle, handle: handle.id,
data, data,
requestId, requestId,
}); });
@@ -143,25 +143,25 @@ export class OPFSFilesystem implements FileSystem<number, number> {
} }
read( read(
handle: number, handle: {id: number, filename: string},
offset: number, offset: number,
length: number, length: number,
): Effect.Effect<Uint8Array, FSErr, never> { ): Effect.Effect<Uint8Array, FSErr, never> {
return Effect.async((cb) => { return Effect.async((cb) => {
const requestId = this.nextRequestId++; const requestId = this.nextRequestId++;
performance.mark("read" + requestId); performance.mark("read" + "_" + handle.filename);
this.callbacks.set(requestId, (event) => { this.callbacks.set(requestId, (event) => {
performance.mark("readEnd" + requestId); performance.mark("readEnd" + "_" + handle.filename);
performance.measure( performance.measure(
"read" + requestId, "read" + "_" + handle.filename,
"read" + requestId, "read" + "_" + handle.filename,
"readEnd" + requestId, "readEnd" + "_" + handle.filename,
); );
cb(Effect.succeed(event.data.data)); cb(Effect.succeed(event.data.data));
}); });
this.opfsWorker.postMessage({ this.opfsWorker.postMessage({
type: "read", type: "read",
handle, handle: handle.id,
offset, offset,
length, length,
requestId, requestId,
@@ -169,46 +169,48 @@ export class OPFSFilesystem implements FileSystem<number, number> {
}); });
} }
close(handle: number): Effect.Effect<void, FSErr, never> { close(handle: {id: number, filename: string}): Effect.Effect<void, FSErr, never> {
return Effect.async((cb) => { return Effect.async((cb) => {
const requestId = this.nextRequestId++; const requestId = this.nextRequestId++;
performance.mark("close" + requestId); performance.mark("close" + "_" + handle.filename);
this.callbacks.set(requestId, (_) => { this.callbacks.set(requestId, (_) => {
performance.mark("closeEnd" + requestId); performance.mark("closeEnd" + "_" + handle.filename);
performance.measure( performance.measure(
"close" + requestId, "close" + "_" + handle.filename,
"close" + requestId, "close" + "_" + handle.filename,
"closeEnd" + requestId, "closeEnd" + "_" + handle.filename,
); );
cb(Effect.succeed(undefined)); cb(Effect.succeed(undefined));
}); });
this.opfsWorker.postMessage({ this.opfsWorker.postMessage({
type: "close", type: "close",
handle, handle: handle.id,
requestId, requestId,
}); });
}); });
} }
closeAndRename( closeAndRename(
handle: number, handle: {id: number, filename: string},
filename: BlockFilename, filename: BlockFilename,
): Effect.Effect<void, FSErr, never> { ): Effect.Effect<void, FSErr, never> {
return Effect.async((cb) => { return Effect.async((cb) => {
const requestId = this.nextRequestId++; const requestId = this.nextRequestId++;
performance.mark("closeAndRename" + requestId); performance.mark("closeAndRename" + "_" + handle.filename);
this.callbacks.set(requestId, () => { this.callbacks.set(requestId, () => {
performance.mark("closeAndRenameEnd" + requestId); performance.mark(
"closeAndRenameEnd" + "_" + handle.filename,
);
performance.measure( performance.measure(
"closeAndRename" + requestId, "closeAndRename" + "_" + handle.filename,
"closeAndRename" + requestId, "closeAndRename" + "_" + handle.filename,
"closeAndRenameEnd" + requestId, "closeAndRenameEnd" + "_" + handle.filename,
); );
cb(Effect.succeed(undefined)); cb(Effect.succeed(undefined));
}); });
this.opfsWorker.postMessage({ this.opfsWorker.postMessage({
type: "closeAndRename", type: "closeAndRename",
handle, handle: handle.id,
filename, filename,
requestId, requestId,
}); });
@@ -220,13 +222,13 @@ export class OPFSFilesystem implements FileSystem<number, number> {
): Effect.Effect<void, FSErr, never> { ): Effect.Effect<void, FSErr, never> {
return Effect.async((cb) => { return Effect.async((cb) => {
const requestId = this.nextRequestId++; const requestId = this.nextRequestId++;
performance.mark("removeFile" + requestId); performance.mark("removeFile" + "_" + filename);
this.callbacks.set(requestId, () => { this.callbacks.set(requestId, () => {
performance.mark("removeFileEnd" + requestId); performance.mark("removeFileEnd" + "_" + filename);
performance.measure( performance.measure(
"removeFile" + requestId, "removeFile" + "_" + filename,
"removeFile" + requestId, "removeFile" + "_" + filename,
"removeFileEnd" + requestId, "removeFileEnd" + "_" + filename,
); );
cb(Effect.succeed(undefined)); cb(Effect.succeed(undefined));
}); });

View File

@@ -10,10 +10,7 @@ import {
WasmCrypto, WasmCrypto,
CryptoProvider, CryptoProvider,
} from "jazz-tools"; } from "jazz-tools";
import { import { AccountID, LSMStorage } from "cojson";
AccountID,
LSMStorage,
} from "cojson";
import { AuthProvider } from "./auth/auth.js"; import { AuthProvider } from "./auth/auth.js";
import { OPFSFilesystem } from "./OPFSFilesystem.js"; import { OPFSFilesystem } from "./OPFSFilesystem.js";
import { IDBStorage } from "cojson-storage-indexeddb"; import { IDBStorage } from "cojson-storage-indexeddb";
@@ -39,7 +36,7 @@ export async function createJazzBrowserContext<Acc extends Account>({
auth: AuthProvider<Acc>; auth: AuthProvider<Acc>;
peer: `wss://${string}` | `ws://${string}`; peer: `wss://${string}` | `ws://${string}`;
reconnectionTimeout?: number; reconnectionTimeout?: number;
storage?: "indexedDB" | "experimentalOPFSdoNotUseOrYouWillBeFired"; storage?: "indexedDB" | "singleTabOPFS";
crypto?: CryptoProvider; crypto?: CryptoProvider;
}): Promise<BrowserContext<Acc>> { }): Promise<BrowserContext<Acc>> {
const crypto = customCrypto || (await WasmCrypto.create()); const crypto = customCrypto || (await WasmCrypto.create());

View File

@@ -1,5 +1,35 @@
# jazz-autosub # jazz-autosub
## 0.7.23
### Patch Changes
- Updated dependencies
- cojson@0.7.23
- jazz-tools@0.7.23
- cojson-transport-ws@0.7.23
## 0.7.22
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.22
## 0.7.21
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.21
## 0.7.20
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.20
## 0.7.19 ## 0.7.19
### Patch Changes ### Patch Changes

View File

@@ -5,7 +5,7 @@
"types": "src/index.ts", "types": "src/index.ts",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"version": "0.7.19", "version": "0.7.23",
"dependencies": { "dependencies": {
"cojson": "workspace:*", "cojson": "workspace:*",
"cojson-transport-ws": "workspace:*", "cojson-transport-ws": "workspace:*",

View File

@@ -1,5 +1,37 @@
# jazz-react # jazz-react
## 0.7.23
### Patch Changes
- Mostly complete OPFS implementation (single-tab only)
- Updated dependencies
- cojson@0.7.23
- jazz-tools@0.7.23
- jazz-browser@0.7.23
## 0.7.22
### Patch Changes
- jazz-browser@0.7.22
## 0.7.21
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.21
- jazz-browser@0.7.21
## 0.7.20
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.20
- jazz-browser@0.7.20
## 0.7.19 ## 0.7.19
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "jazz-react", "name": "jazz-react",
"version": "0.7.19", "version": "0.7.23",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -104,7 +104,10 @@ const DemoAuthBasicUI = ({
signUp: (username: string) => void; signUp: (username: string) => void;
}) => { }) => {
const [username, setUsername] = useState<string>(""); const [username, setUsername] = useState<string>("");
const darkMode = typeof window !== 'undefined' ? window.matchMedia("(prefers-color-scheme: dark)").matches : false; const darkMode =
typeof window !== "undefined"
? window.matchMedia("(prefers-color-scheme: dark)").matches
: false;
return ( return (
<div <div

View File

@@ -23,7 +23,7 @@ export function createJazzReactContext<Acc extends Account>({
}: { }: {
auth: ReactAuthHook<Acc>; auth: ReactAuthHook<Acc>;
peer: `wss://${string}` | `ws://${string}`; peer: `wss://${string}` | `ws://${string}`;
storage?: "indexedDB" | "experimentalOPFSdoNotUseOrYouWillBeFired"; storage?: "indexedDB" | "singleTabOPFS";
}): JazzReactContext<Acc> { }): JazzReactContext<Acc> {
const JazzContext = React.createContext< const JazzContext = React.createContext<
| { | {

View File

@@ -1,5 +1,35 @@
# jazz-autosub # jazz-autosub
## 0.7.23
### Patch Changes
- Updated dependencies
- cojson@0.7.23
- jazz-tools@0.7.23
- cojson-transport-ws@0.7.23
## 0.7.22
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.22
## 0.7.21
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.21
## 0.7.20
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.20
## 0.7.19 ## 0.7.19
### Patch Changes ### Patch Changes

View File

@@ -3,7 +3,7 @@
"bin": "./dist/index.js", "bin": "./dist/index.js",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"version": "0.7.19", "version": "0.7.23",
"scripts": { "scripts": {
"lint": "eslint . --ext ts,tsx", "lint": "eslint . --ext ts,tsx",
"format": "prettier --write './src/**/*.{ts,tsx}'", "format": "prettier --write './src/**/*.{ts,tsx}'",

View File

@@ -3,7 +3,7 @@ import { Command, Options } from "@effect/cli";
import { NodeContext, NodeRuntime } from "@effect/platform-node"; import { NodeContext, NodeRuntime } from "@effect/platform-node";
import { Console, Effect } from "effect"; import { Console, Effect } from "effect";
import { createWebSocketPeer } from "cojson-transport-ws"; import { createWebSocketPeer } from "cojson-transport-ws";
import { WebSocket } from "ws" import { WebSocket } from "ws";
import { import {
Account, Account,
WasmCrypto, WasmCrypto,

View File

@@ -1,5 +1,25 @@
# jazz-autosub # jazz-autosub
## 0.7.23
### Patch Changes
- Mostly complete OPFS implementation (single-tab only)
- Updated dependencies
- cojson@0.7.23
## 0.7.21
### Patch Changes
- Fix another bug in CoMap 'has' proxy trap
## 0.7.20
### Patch Changes
- Fix bug in CoMap 'has' trap
## 0.7.19 ## 0.7.19
### Patch Changes ### Patch Changes

View File

@@ -5,7 +5,7 @@
"types": "./src/index.ts", "types": "./src/index.ts",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"version": "0.7.19", "version": "0.7.23",
"dependencies": { "dependencies": {
"@effect/schema": "^0.66.16", "@effect/schema": "^0.66.16",
"cojson": "workspace:*", "cojson": "workspace:*",

View File

@@ -224,11 +224,13 @@ export class Account extends CoValueBase implements CoValue {
}, },
) { ) {
// TODO: is there a cleaner way to do this? // TODO: is there a cleaner way to do this?
const connectedPeers = await Effect.runPromise(cojsonInternals.connectedPeers( const connectedPeers = await Effect.runPromise(
"creatingAccount", cojsonInternals.connectedPeers(
"createdAccount", "creatingAccount",
{ peer1role: "server", peer2role: "client" }, "createdAccount",
)); { peer1role: "server", peer2role: "client" },
),
);
as._raw.core.node.syncManager.addPeer(connectedPeers[1]); as._raw.core.node.syncManager.addPeer(connectedPeers[1]);

View File

@@ -611,10 +611,10 @@ const CoMapProxyHandler: ProxyHandler<CoMap> = {
} }
}, },
has(target, key) { has(target, key) {
const descriptor = (target._schema[key as keyof CoMap["_schema"]] || const descriptor = (target._schema?.[key as keyof CoMap["_schema"]] ||
target._schema[ItemsSym]) as Schema; target._schema?.[ItemsSym]) as Schema;
if (typeof key === "string" && descriptor) { if (target._raw && typeof key === "string" && descriptor) {
return target._raw.get(key) !== undefined; return target._raw.get(key) !== undefined;
} else { } else {
return Reflect.has(target, key); return Reflect.has(target, key);

View File

@@ -17,6 +17,36 @@ export type IfCo<C, R> = C extends infer _A | infer B
: never; : never;
export type UnCo<T> = T extends co<infer A> ? A : T; export type UnCo<T> = T extends co<infer A> ? A : T;
const optional = {
ref: optionalRef,
json<T extends JsonValue>(): co<T | undefined> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return { [SchemaInit]: "json" satisfies Schema } as any;
},
encoded<T>(arg: OptionalEncoder<T>): co<T> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return { [SchemaInit]: { encoded: arg } satisfies Schema } as any;
},
string: {
[SchemaInit]: "json" satisfies Schema,
} as unknown as co<string | undefined>,
number: {
[SchemaInit]: "json" satisfies Schema,
} as unknown as co<number | undefined>,
boolean: {
[SchemaInit]: "json" satisfies Schema,
} as unknown as co<boolean | undefined>,
null: {
[SchemaInit]: "json" satisfies Schema,
} as unknown as co<null | undefined>,
literal<T extends (string | number | boolean)[]>(
..._lit: T
): co<T[number] | undefined> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return { [SchemaInit]: "json" satisfies Schema } as any;
},
};
/** @category Schema definition */ /** @category Schema definition */
export const co = { export const co = {
string: { string: {
@@ -48,8 +78,15 @@ export const co = {
ref, ref,
items: ItemsSym as ItemsSym, items: ItemsSym as ItemsSym,
members: MembersSym as MembersSym, members: MembersSym as MembersSym,
optional,
}; };
function optionalRef<C extends CoValueClass>(
arg: C | ((_raw: InstanceType<C>["_raw"]) => C),
): co<InstanceType<C> | null | undefined> {
return ref(arg, { optional: true });
}
function ref<C extends CoValueClass>( function ref<C extends CoValueClass>(
arg: C | ((_raw: InstanceType<C>["_raw"]) => C), arg: C | ((_raw: InstanceType<C>["_raw"]) => C),
): co<InstanceType<C> | null>; ): co<InstanceType<C> | null>;
@@ -131,6 +168,10 @@ export type EffectSchemaWithInputAndOutput<A, I = A> = EffectSchema<
}; };
export type Encoder<V> = EffectSchemaWithInputAndOutput<V, JsonValue>; export type Encoder<V> = EffectSchemaWithInputAndOutput<V, JsonValue>;
export type OptionalEncoder<V> = EffectSchemaWithInputAndOutput<
V,
JsonValue | undefined
>;
import { Date } from "@effect/schema/Schema"; import { Date } from "@effect/schema/Schema";
import { SchemaInit, ItemsSym, MembersSym } from "./symbols.js"; import { SchemaInit, ItemsSym, MembersSym } from "./symbols.js";

View File

@@ -157,11 +157,12 @@ describe("CoList resolution", async () => {
test("Loading and availability", async () => { test("Loading and availability", async () => {
const { me, list } = await initNodeAndList(); const { me, list } = await initNodeAndList();
const [initialAsPeer, secondPeer] = await Effect.runPromise(connectedPeers( const [initialAsPeer, secondPeer] = await Effect.runPromise(
"initial", connectedPeers("initial", "second", {
"second", peer1role: "server",
{ peer1role: "server", peer2role: "client" }, peer2role: "client",
)); }),
);
if (!isControlledAccount(me)) { if (!isControlledAccount(me)) {
throw "me is not a controlled account"; throw "me is not a controlled account";
} }
@@ -216,11 +217,12 @@ describe("CoList resolution", async () => {
test("Subscription & auto-resolution", async () => { test("Subscription & auto-resolution", async () => {
const { me, list } = await initNodeAndList(); const { me, list } = await initNodeAndList();
const [initialAsPeer, secondPeer] = await Effect.runPromise(connectedPeers( const [initialAsPeer, secondPeer] = await Effect.runPromise(
"initial", connectedPeers("initial", "second", {
"second", peer1role: "server",
{ peer1role: "server", peer2role: "client" }, peer2role: "client",
)); }),
);
if (!isControlledAccount(me)) { if (!isControlledAccount(me)) {
throw "me is not a controlled account"; throw "me is not a controlled account";
} }

View File

@@ -25,7 +25,7 @@ describe("Simple CoMap operations", async () => {
_height = co.number; _height = co.number;
birthday = co.encoded(Encoders.Date); birthday = co.encoded(Encoders.Date);
name? = co.string; name? = co.string;
nullable = co.encoded(Schema.NullOr(Schema.String)); nullable = co.optional.encoded(Schema.NullishOr(Schema.String));
get roughColor() { get roughColor() {
return this.color + "ish"; return this.color + "ish";
@@ -94,6 +94,8 @@ describe("Simple CoMap operations", async () => {
map.nullable = "not null"; map.nullable = "not null";
map.nullable = null; map.nullable = null;
delete map.nullable;
map.nullable = undefined;
map.name = "Secret name"; map.name = "Secret name";
expect(map.name).toEqual("Secret name"); expect(map.name).toEqual("Secret name");
@@ -442,7 +444,7 @@ describe("CoMap resolution", async () => {
class TestMapWithOptionalRef extends CoMap { class TestMapWithOptionalRef extends CoMap {
color = co.string; color = co.string;
nested = co.ref(NestedMap, { optional: true }); nested = co.optional.ref(NestedMap);
} }
test("Construction with optional", async () => { test("Construction with optional", async () => {

View File

@@ -39,10 +39,12 @@ describe("Deep loading with depth arg", async () => {
crypto: Crypto, crypto: Crypto,
}); });
const [initialAsPeer, secondPeer] = await Effect.runPromise(connectedPeers("initial", "second", { const [initialAsPeer, secondPeer] = await Effect.runPromise(
peer1role: "server", connectedPeers("initial", "second", {
peer2role: "client", peer1role: "server",
})); peer2role: "client",
}),
);
if (!isControlledAccount(me)) { if (!isControlledAccount(me)) {
throw "me is not a controlled account"; throw "me is not a controlled account";
} }
@@ -252,10 +254,12 @@ test("Deep loading a record-like coMap", async () => {
crypto: Crypto, crypto: Crypto,
}); });
const [initialAsPeer, secondPeer] = await Effect.runPromise(connectedPeers("initial", "second", { const [initialAsPeer, secondPeer] = await Effect.runPromise(
peer1role: "server", connectedPeers("initial", "second", {
peer2role: "client", peer1role: "server",
})); peer2role: "client",
}),
);
if (!isControlledAccount(me)) { if (!isControlledAccount(me)) {
throw "me is not a controlled account"; throw "me is not a controlled account";
} }