Compare commits

..

1 Commits

Author SHA1 Message Date
Anselm
71f68b0f60 Try using react-hook-form 2024-08-09 15:09:33 +01:00
74 changed files with 2581 additions and 4441 deletions

View File

@@ -1,5 +0,0 @@
---
"cojson": patch
---
Start introducing neverthrow, make tryAddNewTransactionsAsync and handleNewContent less throwy

View File

@@ -1,5 +0,0 @@
---
"cojson": patch
---
Only one async transaction per CoValue at a time again

View File

@@ -1,27 +0,0 @@
{
"mode": "pre",
"tag": "neverthrow",
"initialVersions": {
"jazz-example-chat": "0.0.80",
"jazz-inspector": "0.0.58",
"jazz-example-pets": "0.0.98",
"jazz-example-todo": "0.0.97",
"cojson": "0.7.33",
"cojson-storage-indexeddb": "0.7.33",
"cojson-storage-sqlite": "0.7.33",
"cojson-transport-ws": "0.7.33",
"hash-slash": "0.2.0",
"jazz-browser": "0.7.33",
"jazz-browser-media-images": "0.7.33",
"jazz-nodejs": "0.7.33",
"jazz-react": "0.7.33",
"jazz-run": "0.7.33",
"jazz-tools": "0.7.33"
},
"changesets": [
"early-files-lick",
"mighty-cameras-tickle",
"rich-tigers-cross",
"tidy-rice-speak"
]
}

View File

@@ -1,5 +0,0 @@
---
"cojson-transport-ws": patch
---
Remove WS buffer filling log message

View File

@@ -1,5 +0,0 @@
---
"cojson": patch
---
Better log message for failed transactions

View File

@@ -3,6 +3,8 @@ name: Build and Deploy
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build-examples:

View File

@@ -1,154 +1,5 @@
# jazz-example-chat
## 0.0.81-neverthrow.3
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.3
- jazz-react@0.7.34-neverthrow.3
- jazz-tools@0.7.34-neverthrow.3
## 0.0.81-neverthrow.2
### Patch Changes
- jazz-react@0.7.34-neverthrow.2
## 0.0.81-neverthrow.1
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.1
- jazz-react@0.7.34-neverthrow.1
- jazz-tools@0.7.34-neverthrow.1
## 0.0.81-neverthrow.0
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.0
- jazz-react@0.7.34-neverthrow.0
- jazz-tools@0.7.34-neverthrow.0
## 0.0.80
### Patch Changes
- Updated dependencies [b297c93]
- Updated dependencies [3bf5127]
- Updated dependencies [a8b74ff]
- Updated dependencies [db53161]
- cojson@0.7.33
- jazz-react@0.7.33
- jazz-tools@0.7.33
## 0.0.80-hotfixes.5
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.5
- jazz-react@0.7.33-hotfixes.5
- jazz-tools@0.7.33-hotfixes.5
## 0.0.80-hotfixes.4
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.4
- jazz-react@0.7.33-hotfixes.4
- jazz-tools@0.7.33-hotfixes.4
## 0.0.80-hotfixes.3
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.3
- jazz-react@0.7.33-hotfixes.3
- jazz-tools@0.7.33-hotfixes.3
## 0.0.80-hotfixes.2
### Patch Changes
- jazz-react@0.7.33-hotfixes.2
## 0.0.80-hotfixes.1
### Patch Changes
- jazz-react@0.7.33-hotfixes.1
## 0.0.80-hotfixes.0
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.0
- jazz-react@0.7.33-hotfixes.0
- jazz-tools@0.7.33-hotfixes.0
## 0.0.79
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.32
- jazz-react@0.7.32
## 0.0.78
### Patch Changes
- Updated dependencies
- cojson@0.7.31
- jazz-react@0.7.31
- jazz-tools@0.7.31
## 0.0.77
### Patch Changes
- jazz-react@0.7.30
## 0.0.76
### Patch Changes
- Updated dependencies
- cojson@0.7.29
- jazz-react@0.7.29
- jazz-tools@0.7.29
## 0.0.75
### Patch Changes
- Updated dependencies
- cojson@0.7.28
- jazz-react@0.7.28
- jazz-tools@0.7.28
## 0.0.74
### Patch Changes
- jazz-react@0.7.27
## 0.0.73
### Patch Changes
- Updated dependencies
- cojson@0.7.26
- jazz-react@0.7.26
- jazz-tools@0.7.26
## 0.0.72
### Patch Changes

View File

@@ -1,13 +1,12 @@
# Jazz Chat Example
Live version: [https://chat.jazz.tools](https://chat.jazz.tools)
Live version: https://example-chat.jazz.tools
## Installing & running the example locally
(this requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation))
Start by checking out `jazz`
```bash
git clone https://github.com/gardencmp/jazz.git
cd jazz/examples/chat
@@ -35,6 +34,7 @@ pnpm dev
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
## Configuration: sync server
By default, the example app uses [Jazz Global Mesh](https://jazz.tools/mesh) (`wss://sync.jazz.tools`) - so cross-device use, invites and collaboration should just work.

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-chat",
"private": true,
"version": "0.0.81-neverthrow.3",
"version": "0.0.72",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -3,7 +3,6 @@ import { createJazzReactContext, DemoAuth } from "jazz-react";
import { createRoot } from "react-dom/client";
import { useIframeHashRouter } from "hash-slash";
import { ChatScreen } from "./chatScreen.tsx";
import { StrictMode } from "react";
export class Message extends CoMap {
text = co.string;
@@ -40,4 +39,4 @@ function App() {
}
createRoot(document.getElementById("root")!)
.render(<StrictMode><Jazz.Provider><App/></Jazz.Provider></StrictMode>);
.render(<Jazz.Provider><App/></Jazz.Provider>);

View File

@@ -1,142 +1,5 @@
# jazz-example-chat
## 0.0.59-neverthrow.3
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.3
- cojson-transport-ws@0.7.34-neverthrow.3
## 0.0.59-neverthrow.2
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.34-neverthrow.2
## 0.0.59-neverthrow.1
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.1
- cojson-transport-ws@0.7.34-neverthrow.1
## 0.0.59-neverthrow.0
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.0
- cojson-transport-ws@0.7.34-neverthrow.0
## 0.0.58
### Patch Changes
- Updated dependencies [fdde8db]
- Updated dependencies [b297c93]
- Updated dependencies [07fe2b9]
- Updated dependencies [3bf5127]
- Updated dependencies [a8b74ff]
- Updated dependencies [db53161]
- cojson-transport-ws@0.7.33
- cojson@0.7.33
## 0.0.58-hotfixes.5
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.5
- cojson-transport-ws@0.7.33-hotfixes.5
## 0.0.58-hotfixes.4
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.4
- cojson-transport-ws@0.7.33-hotfixes.4
## 0.0.58-hotfixes.3
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.33-hotfixes.3
- cojson@0.7.33-hotfixes.3
## 0.0.58-hotfixes.2
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.33-hotfixes.2
## 0.0.58-hotfixes.1
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.33-hotfixes.1
## 0.0.58-hotfixes.0
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.0
- cojson-transport-ws@0.7.33-hotfixes.0
## 0.0.57
### Patch Changes
- Updated dependencies
- Updated dependencies
- cojson-transport-ws@0.7.31
- cojson@0.7.31
## 0.0.56
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.30
## 0.0.55
### Patch Changes
- Updated dependencies
- cojson@0.7.29
- cojson-transport-ws@0.7.29
## 0.0.54
### Patch Changes
- Updated dependencies
- cojson@0.7.28
- cojson-transport-ws@0.7.28
## 0.0.53
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.27
## 0.0.52
### Patch Changes
- Updated dependencies
- cojson@0.7.26
- cojson-transport-ws@0.7.26
## 0.0.51
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-inspector",
"private": true,
"version": "0.0.59-neverthrow.3",
"version": "0.0.51",
"type": "module",
"scripts": {
"dev": "vite",
@@ -19,6 +19,7 @@
"hash-slash": "workspace:*",
"cojson": "workspace:*",
"cojson-transport-ws": "workspace:*",
"effect": "^3.5.2",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",

View File

@@ -10,6 +10,7 @@ import {
WasmCrypto,
} from "cojson";
import { createWebSocketPeer } from "cojson-transport-ws";
import { Effect } from "effect";
import { Trash2 } from "lucide-react";
import { Breadcrumbs } from "./breadcrumbs";
import { usePagePath } from "./use-page-path";
@@ -61,11 +62,13 @@ export default function CoJsonViewerApp() {
}
WasmCrypto.create().then(async (crypto) => {
const wsPeer = createWebSocketPeer({
id: "mesh",
websocket: new WebSocket("wss://mesh.jazz.tools"),
role: "server",
});
const wsPeer = await Effect.runPromise(
createWebSocketPeer({
id: "mesh",
websocket: new WebSocket("wss://mesh.jazz.tools"),
role: "server",
}),
);
const node = await LocalNode.withLoadedAccount({
accountID: currentAccount.id,
accountSecret: currentAccount.secret,

View File

@@ -1,146 +1,5 @@
# jazz-example-pets
## 0.0.99-neverthrow.3
### Patch Changes
- jazz-react@0.7.34-neverthrow.3
- jazz-tools@0.7.34-neverthrow.3
- jazz-browser-media-images@0.7.34-neverthrow.3
## 0.0.99-neverthrow.2
### Patch Changes
- jazz-browser-media-images@0.7.34-neverthrow.2
- jazz-react@0.7.34-neverthrow.2
## 0.0.99-neverthrow.1
### Patch Changes
- jazz-react@0.7.34-neverthrow.1
- jazz-tools@0.7.34-neverthrow.1
- jazz-browser-media-images@0.7.34-neverthrow.1
## 0.0.99-neverthrow.0
### Patch Changes
- jazz-react@0.7.34-neverthrow.0
- jazz-tools@0.7.34-neverthrow.0
- jazz-browser-media-images@0.7.34-neverthrow.0
## 0.0.98
### Patch Changes
- jazz-react@0.7.33
- jazz-tools@0.7.33
- jazz-browser-media-images@0.7.33
## 0.0.98-hotfixes.5
### Patch Changes
- jazz-react@0.7.33-hotfixes.5
- jazz-tools@0.7.33-hotfixes.5
- jazz-browser-media-images@0.7.33-hotfixes.5
## 0.0.98-hotfixes.4
### Patch Changes
- jazz-react@0.7.33-hotfixes.4
- jazz-tools@0.7.33-hotfixes.4
- jazz-browser-media-images@0.7.33-hotfixes.4
## 0.0.98-hotfixes.3
### Patch Changes
- jazz-react@0.7.33-hotfixes.3
- jazz-tools@0.7.33-hotfixes.3
- jazz-browser-media-images@0.7.33-hotfixes.3
## 0.0.98-hotfixes.2
### Patch Changes
- jazz-browser-media-images@0.7.33-hotfixes.2
- jazz-react@0.7.33-hotfixes.2
## 0.0.98-hotfixes.1
### Patch Changes
- jazz-browser-media-images@0.7.33-hotfixes.1
- jazz-react@0.7.33-hotfixes.1
## 0.0.98-hotfixes.0
### Patch Changes
- jazz-react@0.7.33-hotfixes.0
- jazz-tools@0.7.33-hotfixes.0
- jazz-browser-media-images@0.7.33-hotfixes.0
## 0.0.97
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.32
- jazz-browser-media-images@0.7.32
- jazz-react@0.7.32
## 0.0.96
### Patch Changes
- jazz-react@0.7.31
- jazz-tools@0.7.31
- jazz-browser-media-images@0.7.31
## 0.0.95
### Patch Changes
- jazz-browser-media-images@0.7.30
- jazz-react@0.7.30
## 0.0.94
### Patch Changes
- jazz-react@0.7.29
- jazz-tools@0.7.29
- jazz-browser-media-images@0.7.29
## 0.0.93
### Patch Changes
- jazz-react@0.7.28
- jazz-tools@0.7.28
- jazz-browser-media-images@0.7.28
## 0.0.92
### Patch Changes
- jazz-browser-media-images@0.7.27
- jazz-react@0.7.27
## 0.0.91
### Patch Changes
- Updated dependencies
- jazz-react@0.7.26
- jazz-tools@0.7.26
- jazz-browser-media-images@0.7.26
## 0.0.90
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-pets",
"private": true,
"version": "0.0.99-neverthrow.3",
"version": "0.0.90",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,128 +1,5 @@
# jazz-example-todo
## 0.0.98-neverthrow.3
### Patch Changes
- jazz-react@0.7.34-neverthrow.3
- jazz-tools@0.7.34-neverthrow.3
## 0.0.98-neverthrow.2
### Patch Changes
- jazz-react@0.7.34-neverthrow.2
## 0.0.98-neverthrow.1
### Patch Changes
- jazz-react@0.7.34-neverthrow.1
- jazz-tools@0.7.34-neverthrow.1
## 0.0.98-neverthrow.0
### Patch Changes
- jazz-react@0.7.34-neverthrow.0
- jazz-tools@0.7.34-neverthrow.0
## 0.0.97
### Patch Changes
- jazz-react@0.7.33
- jazz-tools@0.7.33
## 0.0.97-hotfixes.5
### Patch Changes
- jazz-react@0.7.33-hotfixes.5
- jazz-tools@0.7.33-hotfixes.5
## 0.0.97-hotfixes.4
### Patch Changes
- jazz-react@0.7.33-hotfixes.4
- jazz-tools@0.7.33-hotfixes.4
## 0.0.97-hotfixes.3
### Patch Changes
- jazz-react@0.7.33-hotfixes.3
- jazz-tools@0.7.33-hotfixes.3
## 0.0.97-hotfixes.2
### Patch Changes
- jazz-react@0.7.33-hotfixes.2
## 0.0.97-hotfixes.1
### Patch Changes
- jazz-react@0.7.33-hotfixes.1
## 0.0.97-hotfixes.0
### Patch Changes
- jazz-react@0.7.33-hotfixes.0
- jazz-tools@0.7.33-hotfixes.0
## 0.0.96
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.32
- jazz-react@0.7.32
## 0.0.95
### Patch Changes
- jazz-react@0.7.31
- jazz-tools@0.7.31
## 0.0.94
### Patch Changes
- jazz-react@0.7.30
## 0.0.93
### Patch Changes
- jazz-react@0.7.29
- jazz-tools@0.7.29
## 0.0.92
### Patch Changes
- jazz-react@0.7.28
- jazz-tools@0.7.28
## 0.0.91
### Patch Changes
- jazz-react@0.7.27
## 0.0.90
### Patch Changes
- Updated dependencies
- jazz-react@0.7.26
- jazz-tools@0.7.26
## 0.0.89
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.98-neverthrow.3",
"version": "0.0.89",
"type": "module",
"scripts": {
"dev": "vite",
@@ -27,6 +27,7 @@
"qrcode": "^1.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.52.2",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"tailwind-merge": "^1.14.0",

View File

@@ -6,7 +6,7 @@ import {
} from "react-router-dom";
import "./index.css";
import { createJazzReactContext, PasskeyAuth } from "jazz-react";
import { createJazzReactContext, DemoAuth, PasskeyAuth } from "jazz-react";
import {
Button,
@@ -31,9 +31,8 @@ import { TodoAccount, TodoProject } from "./1_schema.ts";
const appName = "Jazz Todo List Example";
const auth = PasskeyAuth<TodoAccount>({
const auth = DemoAuth<TodoAccount>({
appName,
Component: PrettyAuthUI,
accountSchema: TodoAccount,
});
const Jazz = createJazzReactContext<TodoAccount>({

View File

@@ -19,6 +19,7 @@ import uniqolor from "uniqolor";
import { useParams } from "react-router";
import { ID } from "jazz-tools";
import { useCoState } from "./2_main";
import { useForm } from "react-hook-form";
/** Walkthrough: Reactively rendering a todo project as a table,
* adding and editing tasks
@@ -76,68 +77,50 @@ export function ProjectTodoTable() {
</h1>
<InviteButton value={project} valueHint="project" />
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[40px]">Done</TableHead>
<TableHead>Task</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{project?.tasks?.map(
(task) => task && <TaskRow key={task.id} task={task} />,
)}
<NewTaskInputRow
createTask={createTask}
disabled={!project}
/>
</TableBody>
</Table>
{project?.tasks?.map(
(task) => task && <TaskRow key={task.id} task={task} />,
)}
<NewTaskInputRow createTask={createTask} disabled={!project} />
</div>
);
}
export function TaskRow({ task }: { task: Task | undefined }) {
const { register, handleSubmit, formState } = useForm({
values: task && { text: task.text, done: task.done },
});
return (
<TableRow>
<TableCell>
<Checkbox
className="mt-1"
checked={task?.done}
onCheckedChange={(checked) => {
// Tick or untick the task
// Task is also immutable, but this will update all queries
// that include this task as a reference
if (task) task.done = !!checked;
}}
/>
</TableCell>
<TableCell>
<div className="flex flex-row justify-between items-center gap-2">
{task?.text ? (
<span className={task?.done ? "line-through" : ""}>
{task.text}
</span>
) : (
<Skeleton className="mt-1 w-[200px] h-[1em] rounded-full" />
)}
{
// Here we see for the first time how we can access edit history
// for a CoValue, and use it to display who created the task.
task?._edits.text?.by?.profile?.name ? (
<span
className="rounded-full py-0.5 px-2 text-xs"
style={uniqueColoring(task._edits.text.by.id)}
>
{task._edits.text.by.profile.name}
</span>
) : (
<Skeleton className="mt-1 w-[50px] h-[1em] rounded-full" />
)
}
</div>
</TableCell>
</TableRow>
<form
onSubmit={handleSubmit((newValues) => task?.applyDiff(newValues))}
className="flex gap-2 items-center"
>
<Checkbox className="mt-1" {...register("done")} />
{task?.text ? (
// <span className={task?.done ? "line-through" : ""}>
// {task.text}
// </span>
<input type="text" {...register("text")} />
) : (
<Skeleton className="mt-1 w-[200px] h-[1em] rounded-full" />
)}
{formState.isDirty && <input type="submit" value="Save" />}
{
// Here we see for the first time how we can access edit history
// for a CoValue, and use it to display who created the task.
task?._edits.text?.by?.profile?.name ? (
<span
className="rounded-full py-0.5 px-2 text-xs"
style={uniqueColoring(task._edits.text.by.id)}
>
{task._edits.text.by.profile.name}
</span>
) : (
<Skeleton className="mt-1 w-[50px] h-[1em] rounded-full" />
)
}
</form>
);
}

View File

@@ -268,7 +268,11 @@ Jazz Mesh is currently free &mdash; and it's set up as the default sync & storag
## Get Started
- <Link href="/docs" target="_blank">Read the docs</Link>
- <Link href="https://discord.gg/utDMjHYg42" target="_blank">Join our Discord</Link>
- <Link href="/docs" target="_blank">
Read the docs
</Link>
- <Link href="https://discord.gg/utDMjHYg42" target="_blank">
Join our Discord
</Link>
</Prose>

View File

@@ -1,95 +1,5 @@
# cojson-storage-indexeddb
## 0.7.34-neverthrow.3
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.3
## 0.7.34-neverthrow.1
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.1
## 0.7.34-neverthrow.0
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.0
## 0.7.33
### Patch Changes
- 3bf5127: Allow crashing whole node on peer errors
- Updated dependencies [b297c93]
- Updated dependencies [3bf5127]
- Updated dependencies [a8b74ff]
- Updated dependencies [db53161]
- cojson@0.7.33
## 0.7.33-hotfixes.5
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.5
## 0.7.33-hotfixes.4
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.4
## 0.7.33-hotfixes.3
### Patch Changes
- Allow crashing whole node on peer errors
- Updated dependencies
- cojson@0.7.33-hotfixes.3
## 0.7.33-hotfixes.0
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.0
## 0.7.31
### Patch Changes
- Updated dependencies
- cojson@0.7.31
## 0.7.29
### Patch Changes
- Updated dependencies
- cojson@0.7.29
## 0.7.28
### Patch Changes
- Updated dependencies
- cojson@0.7.28
## 0.7.26
### Patch Changes
- Remove Effect from jazz/cojson internals
- Updated dependencies
- cojson@0.7.26
## 0.7.23
### Patch Changes

View File

@@ -1,12 +1,13 @@
{
"name": "cojson-storage-indexeddb",
"version": "0.7.34-neverthrow.3",
"version": "0.7.23",
"main": "dist/index.js",
"type": "module",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "workspace:*",
"effect": "^3.5.2",
"typescript": "^5.1.6"
},
"devDependencies": {

View File

@@ -10,6 +10,7 @@ import {
OutgoingSyncQueue,
} from "cojson";
import { SyncPromise } from "./syncPromises.js";
import { Effect, Queue, Stream } from "effect";
type CoValueRow = {
id: CojsonInternalTypes.RawCoID;
@@ -52,15 +53,11 @@ export class IDBStorage {
this.db = db;
this.toLocalNode = toLocalNode;
const processMessages = async () => {
for await (const msg of fromLocalNode) {
try {
if (msg === "Disconnected" || msg === "PingTimeout") {
throw new Error("Unexpected Disconnected message");
}
await this.handleSyncMessage(msg);
} catch (e) {
console.error(
void fromLocalNode.pipe(
Stream.runForEach((msg) =>
Effect.tryPromise({
try: () => this.handleSyncMessage(msg),
catch: (e) =>
new Error(
`Error reading from localNode, handling msg\n\n${JSON.stringify(
msg,
@@ -71,13 +68,9 @@ export class IDBStorage {
)}`,
{ cause: e },
),
);
}
}
};
processMessages().catch((e) =>
console.error("Error in processMessages in IndexedDB", e),
}),
),
Effect.runPromise,
);
}
@@ -89,18 +82,25 @@ export class IDBStorage {
localNodeName: "local",
},
): Promise<Peer> {
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
localNodeName,
"storage",
{ peer1role: "client", peer2role: "server", trace, crashOnClose: true },
);
return Effect.runPromise(
Effect.gen(function* () {
const [localNodeAsPeer, storageAsPeer] =
yield* cojsonInternals.connectedPeers(
localNodeName,
"storage",
{ peer1role: "client", peer2role: "server", trace },
);
await IDBStorage.open(
localNodeAsPeer.incoming,
localNodeAsPeer.outgoing,
);
yield* Effect.promise(() =>
IDBStorage.open(
localNodeAsPeer.incoming,
localNodeAsPeer.outgoing,
),
);
return { ...storageAsPeer, priority: 100 };
return { ...storageAsPeer, priority: 100 };
}),
);
}
static async open(
@@ -392,40 +392,35 @@ export class IDBStorage {
),
).then(() => {
// we're done with IndexedDB stuff here so can use native Promises again
setTimeout(() => {
this.toLocalNode
.push({
action: "known",
...ourKnown,
asDependencyOf,
})
.catch((e) =>
console.error(
"Error sending known state",
e,
),
);
setTimeout(() =>
Effect.runPromise(
Effect.gen(this, function* () {
yield* Queue.offer(this.toLocalNode, {
action: "known",
...ourKnown,
asDependencyOf,
});
const nonEmptyNewContentPieces =
newContentPieces.filter(
(piece) =>
piece.header ||
Object.keys(piece.new).length > 0,
);
const nonEmptyNewContentPieces =
newContentPieces.filter(
(piece) =>
piece.header ||
Object.keys(piece.new)
.length > 0,
);
// console.log(theirKnown.id, nonEmptyNewContentPieces);
// console.log(theirKnown.id, nonEmptyNewContentPieces);
for (const piece of nonEmptyNewContentPieces) {
this.toLocalNode
.push(piece)
.catch((e) =>
console.error(
"Error sending new content piece",
e,
),
);
}
});
for (const piece of nonEmptyNewContentPieces) {
yield* Queue.offer(
this.toLocalNode,
piece,
);
yield* Effect.yieldNow();
}
}),
),
);
return Promise.resolve();
});
@@ -450,18 +445,16 @@ export class IDBStorage {
const header = msg.header;
if (!header) {
console.error("Expected to be sent header first");
this.toLocalNode
.push({
void Effect.runPromise(
Queue.offer(this.toLocalNode, {
action: "known",
id: msg.id,
header: false,
sessions: {},
isCorrection: true,
})
.catch((e) =>
console.error("Error sending known state", e),
);
return SyncPromise.resolve();
}),
);
throw new Error("Expected to be sent header first");
}
return this.makeRequest<IDBValidKey>(({ coValues }) =>
@@ -522,18 +515,13 @@ export class IDBStorage {
),
).then(() => {
if (invalidAssumptions) {
this.toLocalNode
.push({
void Effect.runPromise(
Queue.offer(this.toLocalNode, {
action: "known",
...ourKnown,
isCorrection: invalidAssumptions,
})
.catch((e) =>
console.error(
"Error sending known state",
e,
),
);
}),
);
}
});
});

View File

@@ -1,102 +1,5 @@
# cojson-storage-sqlite
## 0.7.34-neverthrow.3
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.3
## 0.7.34-neverthrow.1
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.1
## 0.7.34-neverthrow.0
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.0
## 0.7.33
### Patch Changes
- 3bf5127: Allow crashing whole node on peer errors
- Updated dependencies [b297c93]
- Updated dependencies [3bf5127]
- Updated dependencies [a8b74ff]
- Updated dependencies [db53161]
- cojson@0.7.33
## 0.7.33-hotfixes.6
### Patch Changes
- Get rid of simulated errors
## 0.7.33-hotfixes.5
### Patch Changes
- Make simulated errors even more likely
- Updated dependencies
- cojson@0.7.33-hotfixes.5
## 0.7.33-hotfixes.4
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.4
## 0.7.33-hotfixes.3
### Patch Changes
- Allow crashing whole node on peer errors
- Updated dependencies
- cojson@0.7.33-hotfixes.3
## 0.7.33-hotfixes.0
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.0
## 0.7.31
### Patch Changes
- Updated dependencies
- cojson@0.7.31
## 0.7.29
### Patch Changes
- Updated dependencies
- cojson@0.7.29
## 0.7.28
### Patch Changes
- Updated dependencies
- cojson@0.7.28
## 0.7.26
### Patch Changes
- Remove Effect from jazz/cojson internals
- Updated dependencies
- cojson@0.7.26
## 0.7.23
### Patch Changes

View File

@@ -1,13 +1,14 @@
{
"name": "cojson-storage-sqlite",
"type": "module",
"version": "0.7.34-neverthrow.3",
"version": "0.7.23",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^8.5.2",
"cojson": "workspace:*",
"effect": "^3.5.2",
"typescript": "^5.1.6"
},
"devDependencies": {

View File

@@ -11,6 +11,7 @@ import {
} from "cojson";
import Database, { Database as DatabaseT } from "better-sqlite3";
import { Effect, Queue, Stream } from "effect";
type CoValueRow = {
id: CojsonInternalTypes.RawCoID;
@@ -53,15 +54,11 @@ export class SQLiteStorage {
this.db = db;
this.toLocalNode = toLocalNode;
const processMessages = async () => {
for await (const msg of fromLocalNode) {
try {
if (msg === "Disconnected" || msg === "PingTimeout") {
throw new Error("Unexpected Disconnected message");
}
await this.handleSyncMessage(msg);
} catch (e) {
console.error(
void fromLocalNode.pipe(
Stream.runForEach((msg) =>
Effect.tryPromise({
try: () => this.handleSyncMessage(msg),
catch: (e) =>
new Error(
`Error reading from localNode, handling msg\n\n${JSON.stringify(
msg,
@@ -72,13 +69,9 @@ export class SQLiteStorage {
)}`,
{ cause: e },
),
);
}
}
};
processMessages().catch((e) =>
console.error("Error in processMessages in sqlite", e),
}),
),
Effect.runPromise,
);
}
@@ -91,19 +84,26 @@ export class SQLiteStorage {
trace?: boolean;
localNodeName?: string;
}): Promise<Peer> {
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
localNodeName,
"storage",
{ peer1role: "client", peer2role: "server", trace, crashOnClose: true },
);
return Effect.runPromise(
Effect.gen(function* () {
const [localNodeAsPeer, storageAsPeer] =
yield* cojsonInternals.connectedPeers(
localNodeName,
"storage",
{ peer1role: "client", peer2role: "server", trace },
);
await SQLiteStorage.open(
filename,
localNodeAsPeer.incoming,
localNodeAsPeer.outgoing,
);
yield* Effect.promise(() =>
SQLiteStorage.open(
filename,
localNodeAsPeer.incoming,
localNodeAsPeer.outgoing,
),
);
return { ...storageAsPeer, priority: 100 };
return { ...storageAsPeer, priority: 100 };
}),
);
}
static async open(
@@ -441,13 +441,13 @@ export class SQLiteStorage {
);
}
this.toLocalNode
.push({
await Effect.runPromise(
Queue.offer(this.toLocalNode, {
action: "known",
...ourKnown,
asDependencyOf,
})
.catch((e) => console.error("Error while pushing known", e));
}),
);
const nonEmptyNewContentPieces = newContentPieces.filter(
(piece) => piece.header || Object.keys(piece.new).length > 0,
@@ -456,11 +456,7 @@ export class SQLiteStorage {
// console.log(theirKnown.id, nonEmptyNewContentPieces);
for (const piece of nonEmptyNewContentPieces) {
this.toLocalNode
.push(piece)
.catch((e) =>
console.error("Error while pushing content piece", e),
);
await Effect.runPromise(Queue.offer(this.toLocalNode, piece));
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
@@ -482,17 +478,15 @@ export class SQLiteStorage {
const header = msg.header;
if (!header) {
console.error("Expected to be sent header first");
this.toLocalNode
.push({
await Effect.runPromise(
Queue.offer(this.toLocalNode, {
action: "known",
id: msg.id,
header: false,
sessions: {},
isCorrection: true,
})
.catch((e) =>
console.error("Error while pushing known", e),
);
}),
);
return;
}
@@ -624,13 +618,13 @@ export class SQLiteStorage {
})();
if (invalidAssumptions) {
this.toLocalNode
.push({
await Effect.runPromise(
Queue.offer(this.toLocalNode, {
action: "known",
...ourKnown,
isCorrection: invalidAssumptions,
})
.catch((e) => console.error("Error while pushing known", e));
}),
);
}
}

View File

@@ -1,128 +1,5 @@
# cojson-transport-nodejs-ws
## 0.7.34-neverthrow.3
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.3
## 0.7.34-neverthrow.2
### Patch Changes
- Remove WS buffer filling log message
## 0.7.34-neverthrow.1
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.1
## 0.7.34-neverthrow.0
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.0
## 0.7.33
### Patch Changes
- fdde8db: Handle ws closing while buffering
- 07fe2b9: Wait if WS buffer is full
- 3bf5127: Allow crashing whole node on peer errors
- Updated dependencies [b297c93]
- Updated dependencies [3bf5127]
- Updated dependencies [a8b74ff]
- Updated dependencies [db53161]
- cojson@0.7.33
## 0.7.33-hotfixes.5
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.5
## 0.7.33-hotfixes.4
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.4
## 0.7.33-hotfixes.3
### Patch Changes
- Allow crashing whole node on peer errors
- Updated dependencies
- cojson@0.7.33-hotfixes.3
## 0.7.33-hotfixes.2
### Patch Changes
- Handle ws closing while buffering
## 0.7.33-hotfixes.1
### Patch Changes
- Wait if WS buffer is full
## 0.7.33-hotfixes.0
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.0
## 0.7.31
### Patch Changes
- Properly close connecting websockets
- Updated dependencies
- cojson@0.7.31
## 0.7.30
### Patch Changes
- Immediately resolve an already-open websocket
## 0.7.29
### Patch Changes
- Updated dependencies
- cojson@0.7.29
## 0.7.28
### Patch Changes
- Updated dependencies
- cojson@0.7.28
## 0.7.27
### Patch Changes
- Option to not expect pings
## 0.7.26
### Patch Changes
- Remove Effect from jazz/cojson internals
- Updated dependencies
- cojson@0.7.26
## 0.7.23
### Patch Changes

View File

@@ -1,12 +1,13 @@
{
"name": "cojson-transport-ws",
"type": "module",
"version": "0.7.34-neverthrow.3",
"version": "0.7.23",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "workspace:*",
"effect": "^3.5.2",
"typescript": "^5.1.6"
},
"scripts": {

View File

@@ -1,10 +1,5 @@
import {
DisconnectedError,
Peer,
PingTimeoutError,
SyncMessage,
cojsonInternals,
} from "cojson";
import { DisconnectedError, Peer, PingTimeoutError, SyncMessage } from "cojson";
import { Stream, Queue, Effect, Console } from "effect";
interface WebsocketEvents {
close: { code: number; reason: string };
@@ -20,7 +15,6 @@ interface AnyWebSocket {
addEventListener<K extends keyof WebsocketEvents>(
type: K,
listener: (event: WebsocketEvents[K]) => void,
options?: { once: boolean },
): void;
removeEventListener<K extends keyof WebsocketEvents>(
type: K,
@@ -28,8 +22,6 @@ interface AnyWebSocket {
): void;
close(): void;
send(data: string): void;
readyState: number;
bufferedAmount: number;
}
const g: typeof globalThis & {
@@ -40,99 +32,88 @@ const g: typeof globalThis & {
}[];
} = globalThis;
export function createWebSocketPeer({
id,
websocket,
role,
expectPings = true,
}: {
export function createWebSocketPeer(options: {
id: string;
websocket: AnyWebSocket;
role: Peer["role"];
expectPings?: boolean;
}): Peer {
const incoming = new cojsonInternals.Channel<
SyncMessage | DisconnectedError | PingTimeoutError
>();
}): Effect.Effect<Peer> {
return Effect.gen(function* () {
const ws = options.websocket;
const ws_ = ws as unknown as Stream.EventListener<WebsocketEvents["message"]>;
websocket.addEventListener("close", function handleClose() {
incoming
.push("Disconnected")
.catch((e) =>
console.error("Error while pushing disconnect msg", e),
const outgoing = yield* Queue.unbounded<SyncMessage>();
const closed = once(ws, "close").pipe(
Effect.flatMap(
(event) =>
new DisconnectedError({
message: `${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,
),
),
),
),
Stream.mergeLeft(closed),
Stream.map((_) => JSON.parse(_.data as string)),
Stream.filter(isSyncMessage),
Stream.buffer({ capacity: "unbounded" }),
Stream.onDone(() => Queue.shutdown(outgoing)),
);
return {
id: options.id,
incoming: messages,
outgoing,
role: options.role,
};
});
let pingTimeout: ReturnType<typeof setTimeout> | null = null;
websocket.addEventListener("message", function handleIncomingMsg(event) {
const msg = JSON.parse(event.data as string);
pingTimeout && clearTimeout(pingTimeout);
if (msg?.type === "ping") {
const ping = msg as PingMsg;
g.jazzPings ||= [];
g.jazzPings.push({
received: Date.now(),
sent: ping.time,
dc: ping.dc,
});
} else {
incoming
.push(msg)
.catch((e) =>
console.error("Error while pushing incoming msg", e),
);
}
if (expectPings) {
pingTimeout = setTimeout(() => {
incoming
.push("PingTimeout")
.catch((e) =>
console.error("Error while pushing ping timeout", e),
);
}, 10_000);
}
});
const websocketOpen = new Promise<void>((resolve) => {
if (websocket.readyState === 1) {
resolve();
} else {
websocket.addEventListener("open", resolve, { once: true });
}
});
return {
id,
incoming,
outgoing: {
async push(msg) {
await websocketOpen;
if (websocket.readyState === 1) {
while (websocket.bufferedAmount > 1_000_000) {
await new Promise((resolve) =>
setTimeout(resolve, 100),
);
if (websocket.readyState !== 1) {
console.log("WebSocket closed while buffering", id, websocket.bufferedAmount);
return;
}
}
websocket.send(JSON.stringify(msg));
}
},
close() {
console.log("Trying to close", id, websocket.readyState)
if (websocket.readyState === 0) {
websocket.addEventListener("open", function handleClose() {
websocket.close();
}, { once: true });
} else if (websocket.readyState == 1) {
websocket.close();
}
},
},
role,
crashOnClose: false,
};
}
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

@@ -3,10 +3,10 @@ module.exports = {
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:require-extensions/recommended",
"prettier",
"prettier"
],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "require-extensions", "neverthrow"],
plugins: ["@typescript-eslint", "require-extensions"],
parserOptions: {
project: "./tsconfig.json",
tsconfigRootDir: __dirname,
@@ -20,6 +20,5 @@ module.exports = {
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"@typescript-eslint/no-floating-promises": "error",
"neverthrow/must-use-result": "error",
},
};
}

View File

@@ -1,79 +1,5 @@
# cojson
## 0.7.34-neverthrow.3
### Patch Changes
- Better log message for failed transactions
## 0.7.34-neverthrow.1
### Patch Changes
- Only one async transaction per CoValue at a time again
## 0.7.34-neverthrow.0
### Patch Changes
- Start introducing neverthrow, make tryAddNewTransactionsAsync and handleNewContent less throwy
## 0.7.33
### Patch Changes
- b297c93: Improve logging
- 3bf5127: Allow crashing whole node on peer errors
- a8b74ff: Throw properly on peer that should crash on close
## 0.7.33-hotfixes.5
### Patch Changes
- Make simulated errors even more likely
## 0.7.33-hotfixes.4
### Patch Changes
- Throw properly on peer that should crash on close
## 0.7.33-hotfixes.3
### Patch Changes
- Allow crashing whole node on peer errors
## 0.7.33-hotfixes.0
### Patch Changes
- Improve logging
## 0.7.31
### Patch Changes
- Close both ends of the peer on gracefulShutdown
## 0.7.29
### Patch Changes
- Remove noisy log
## 0.7.28
### Patch Changes
- Fix ignoring server peers
## 0.7.26
### Patch Changes
- Remove Effect from jazz/cojson internals
## 0.7.23
### Patch Changes

View File

@@ -5,27 +5,25 @@
"types": "src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.7.34-neverthrow.3",
"version": "0.7.23",
"devDependencies": {
"@types/jest": "^29.5.3",
"@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^6.2.1",
"eslint": "^8.46.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-neverthrow": "^1.1.4",
"eslint-plugin-require-extensions": "^0.1.3",
"typescript": "5.0.2",
"vitest": "^0.34.6"
},
"dependencies": {
"@hazae41/berith": "^1.2.6",
"@noble/ciphers": "^0.1.3",
"@noble/curves": "^1.3.0",
"@noble/ciphers": "^0.1.3",
"@noble/hashes": "^1.4.0",
"@scure/base": "^1.1.1",
"hash-wasm": "^4.9.0",
"neverthrow": "^7.0.1",
"queueable": "^5.3.2"
"effect": "^3.5.2",
"hash-wasm": "^4.9.0"
},
"scripts": {
"dev": "tsc --watch --sourceMap --outDir dist",

View File

@@ -7,7 +7,6 @@ import {
StreamingHash,
KeyID,
CryptoProvider,
SignerID,
} from "./crypto/crypto.js";
import { JsonObject, JsonValue } from "./jsonValue.js";
import { base58 } from "@scure/base";
@@ -17,7 +16,7 @@ import {
isKeyForKeyField,
} from "./permissions.js";
import { RawGroup } from "./coValues/group.js";
import { LocalNode, ResolveAccountAgentError } from "./localNode.js";
import { LocalNode } from "./localNode.js";
import { CoValueKnownState, NewContentMessage } from "./sync.js";
import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
import { AccountID, ControlledAccountOrAgent } from "./coValues/account.js";
@@ -26,7 +25,6 @@ import { coreToCoValue } from "./coreToCoValue.js";
import { expectGroup } from "./typeUtils/expectGroup.js";
import { isAccountID } from "./typeUtils/isAccountID.js";
import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
import { err, ok, Result, ResultAsync } from "neverthrow";
/**
In order to not block other concurrently syncing CoValues we introduce a maximum size of transactions,
@@ -105,10 +103,10 @@ export class CoValueCore {
_decryptionCache: {
[key: Encrypted<JsonValue[], JsonValue>]: JsonValue[] | undefined;
} = {};
currentlyAsyncApplyingTxDone?: Promise<void>;
_cachedKnownState?: CoValueKnownState;
_cachedDependentOn?: RawCoID[];
_cachedNewContentSinceEmpty?: NewContentMessage[] | undefined;
_currentAsyncAddTransaction?: Promise<void>;
constructor(
header: CoValueHeader,
@@ -184,9 +182,7 @@ export class CoValueCore {
this.header.meta?.type === "account"
? (this.node.currentSessionID.replace(
this.node.account.id,
this.node.account
.currentAgentID()
._unsafeUnwrap({ withStackTrace: true }),
this.node.account.currentAgentID(),
) as SessionID)
: this.node.currentSessionID;
@@ -201,197 +197,166 @@ export class CoValueCore {
newTransactions: Transaction[],
givenExpectedNewHash: Hash | undefined,
newSignature: Signature,
): Result<true, TryAddTransactionsError> {
return this.node
.resolveAccountAgent(
): boolean {
const signerID = this.crypto.getAgentSignerID(
this.node.resolveAccountAgent(
accountOrAgentIDfromSessionID(sessionID),
"Expected to know signer of transaction",
)
.andThen((agent) => {
const signerID = this.crypto.getAgentSignerID(agent);
),
);
// const beforeHash = performance.now();
const { expectedNewHash, newStreamingHash } =
this.expectedNewHashAfter(sessionID, newTransactions);
// const afterHash = performance.now();
// console.log(
// "Hashing took",
// afterHash - beforeHash
// );
if (!signerID) {
console.warn(
"Unknown agent",
accountOrAgentIDfromSessionID(sessionID),
);
return false;
}
if (
givenExpectedNewHash &&
givenExpectedNewHash !== expectedNewHash
) {
return err({
type: "InvalidHash",
id: this.id,
expectedNewHash,
givenExpectedNewHash,
} satisfies InvalidHashError);
}
// const beforeHash = performance.now();
const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
sessionID,
newTransactions,
);
// const afterHash = performance.now();
// console.log(
// "Hashing took",
// afterHash - beforeHash
// );
// const beforeVerify = performance.now();
if (
!this.crypto.verify(newSignature, expectedNewHash, signerID)
) {
return err({
type: "InvalidSignature",
id: this.id,
newSignature,
sessionID,
signerID,
} satisfies InvalidSignatureError);
}
// const afterVerify = performance.now();
// console.log(
// "Verify took",
// afterVerify - beforeVerify
// );
this.doAddTransactions(
sessionID,
newTransactions,
newSignature,
expectedNewHash,
newStreamingHash,
"immediate",
);
return ok(true as const);
if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
console.warn("Invalid hash", {
expectedNewHash,
givenExpectedNewHash,
});
return false;
}
// const beforeVerify = performance.now();
if (!this.crypto.verify(newSignature, expectedNewHash, signerID)) {
console.warn(
"Invalid signature in",
this.id,
newSignature,
expectedNewHash,
signerID,
);
return false;
}
// const afterVerify = performance.now();
// console.log(
// "Verify took",
// afterVerify - beforeVerify
// );
this.doAddTransactions(
sessionID,
newTransactions,
newSignature,
expectedNewHash,
newStreamingHash,
"immediate",
);
return true;
}
tryAddTransactionsAsync(
async tryAddTransactionsAsync(
sessionID: SessionID,
newTransactions: Transaction[],
givenExpectedNewHash: Hash | undefined,
newSignature: Signature,
): ResultAsync<true, TryAddTransactionsError> {
const currentAsyncAddTransaction = this._currentAsyncAddTransaction;
let maybeAwaitPrevious:
| ResultAsync<void, TryAddTransactionsError>
| undefined;
let thisDone = () => {};
): Promise<boolean> {
if (this.currentlyAsyncApplyingTxDone) {
await this.currentlyAsyncApplyingTxDone;
}
let resolveDone!: () => void;
if (currentAsyncAddTransaction) {
// eslint-disable-next-line neverthrow/must-use-result
maybeAwaitPrevious = ResultAsync.fromSafePromise(
currentAsyncAddTransaction,
this.currentlyAsyncApplyingTxDone = new Promise((resolve) => {
resolveDone = resolve;
});
const signerID = this.crypto.getAgentSignerID(
await this.node.resolveAccountAgentAsync(
accountOrAgentIDfromSessionID(sessionID),
"Expected to know signer of transaction",
),
);
if (!signerID) {
console.warn(
"Unknown agent",
accountOrAgentIDfromSessionID(sessionID),
);
} else {
// eslint-disable-next-line neverthrow/must-use-result
maybeAwaitPrevious = ResultAsync.fromSafePromise(Promise.resolve());
this._currentAsyncAddTransaction = new Promise((resolve) => {
thisDone = resolve;
resolveDone();
return false;
}
const nTxBefore =
this.sessionLogs.get(sessionID)?.transactions.length ?? 0;
// const beforeHash = performance.now();
const { expectedNewHash, newStreamingHash } =
await this.expectedNewHashAfterAsync(sessionID, newTransactions);
// const afterHash = performance.now();
// console.log(
// "Hashing took",
// afterHash - beforeHash
// );
const nTxAfter =
this.sessionLogs.get(sessionID)?.transactions.length ?? 0;
if (nTxAfter !== nTxBefore) {
const newTransactionLengthBefore = newTransactions.length;
newTransactions = newTransactions.slice(nTxAfter - nTxBefore);
console.warn("Transactions changed while async hashing", {
nTxBefore,
nTxAfter,
newTransactionLengthBefore,
remainingNewTransactions: newTransactions.length,
});
}
return maybeAwaitPrevious
.andThen((_previousDone) =>
this.node
.resolveAccountAgentAsync(
accountOrAgentIDfromSessionID(sessionID),
"Expected to know signer of transaction",
)
.andThen((agent) => {
const signerID = this.crypto.getAgentSignerID(agent);
const nTxBefore =
this.sessionLogs.get(sessionID)?.transactions
.length ?? 0;
// const beforeHash = performance.now();
return ResultAsync.fromSafePromise(
this.expectedNewHashAfterAsync(
sessionID,
newTransactions,
),
).andThen(({ expectedNewHash, newStreamingHash }) => {
// const afterHash = performance.now();
// console.log(
// "Hashing took",
// afterHash - beforeHash
// );
const nTxAfter =
this.sessionLogs.get(sessionID)?.transactions
.length ?? 0;
if (nTxAfter !== nTxBefore) {
const newTransactionLengthBefore =
newTransactions.length;
newTransactions = newTransactions.slice(
nTxAfter - nTxBefore,
);
console.warn(
"Transactions changed while async hashing",
{
nTxBefore,
nTxAfter,
newTransactionLengthBefore,
remainingNewTransactions:
newTransactions.length,
},
);
}
if (
givenExpectedNewHash &&
givenExpectedNewHash !== expectedNewHash
) {
return err({
type: "InvalidHash",
id: this.id,
expectedNewHash,
givenExpectedNewHash,
} satisfies InvalidHashError);
}
performance.mark("verifyStart" + this.id);
if (
!this.crypto.verify(
newSignature,
expectedNewHash,
signerID,
)
) {
return err({
type: "InvalidSignature",
id: this.id,
newSignature,
sessionID,
signerID,
} satisfies InvalidSignatureError);
}
performance.mark("verifyEnd" + this.id);
performance.measure(
"verify" + this.id,
"verifyStart" + this.id,
"verifyEnd" + this.id,
);
this.doAddTransactions(
sessionID,
newTransactions,
newSignature,
expectedNewHash,
newStreamingHash,
"deferred",
);
return ok(true as const);
});
}),
)
.map((trueResult) => {
thisDone();
return trueResult;
})
.mapErr((err) => {
thisDone();
return err;
if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
console.warn("Invalid hash", {
expectedNewHash,
givenExpectedNewHash,
});
resolveDone();
return false;
}
performance.mark("verifyStart" + this.id);
if (!this.crypto.verify(newSignature, expectedNewHash, signerID)) {
console.warn(
"Invalid signature in",
this.id,
newSignature,
expectedNewHash,
signerID,
);
resolveDone();
return false;
}
performance.mark("verifyEnd" + this.id);
performance.measure(
"verify" + this.id,
"verifyStart" + this.id,
"verifyEnd" + this.id,
);
this.doAddTransactions(
sessionID,
newTransactions,
newSignature,
expectedNewHash,
newStreamingHash,
"deferred",
);
resolveDone();
return true;
}
private doAddTransactions(
@@ -402,9 +367,6 @@ export class CoValueCore {
newStreamingHash: StreamingHash,
notifyMode: "immediate" | "deferred",
) {
if (this.node.crashed) {
throw new Error("Trying to add transactions after node is crashed");
}
const transactions =
this.sessionLogs.get(sessionID)?.transactions ?? [];
transactions.push(...newTransactions);
@@ -580,9 +542,7 @@ export class CoValueCore {
this.header.meta?.type === "account"
? (this.node.currentSessionID.replace(
this.node.account.id,
this.node.account
.currentAgentID()
._unsafeUnwrap({ withStackTrace: true }),
this.node.account.currentAgentID(),
) as SessionID)
: this.node.currentSessionID;
@@ -600,7 +560,7 @@ export class CoValueCore {
[transaction],
expectedNewHash,
signature,
)._unsafeUnwrap({ withStackTrace: true });
);
if (success) {
void this.node.syncManager.syncCoValue(this);
@@ -746,9 +706,7 @@ export class CoValueCore {
// Try to find key revelation for us
const lookupAccountOrAgentID =
this.header.meta?.type === "account"
? this.node.account
.currentAgentID()
._unsafeUnwrap({ withStackTrace: true })
? this.node.account.currentAgentID()
: this.node.account.id;
const lastReadyKeyEdit = content.lastEditAt(
@@ -757,9 +715,10 @@ export class CoValueCore {
if (lastReadyKeyEdit?.value) {
const revealer = lastReadyKeyEdit.by;
const revealerAgent = this.node
.resolveAccountAgent(revealer, "Expected to know revealer")
._unsafeUnwrap({ withStackTrace: true });
const revealerAgent = this.node.resolveAccountAgent(
revealer,
"Expected to know revealer",
);
const secret = this.crypto.unseal(
lastReadyKeyEdit.value,
@@ -1024,23 +983,3 @@ function getNextKnownSignatureIdx(
idx >= (sentStateForSessionID ?? knownStateForSessionID ?? -1),
);
}
export type InvalidHashError = {
type: "InvalidHash";
id: RawCoID;
expectedNewHash: Hash;
givenExpectedNewHash: Hash;
};
export type InvalidSignatureError = {
type: "InvalidSignature";
id: RawCoID;
newSignature: Signature;
sessionID: SessionID;
signerID: SignerID;
};
export type TryAddTransactionsError =
| ResolveAccountAgentError
| InvalidHashError
| InvalidSignatureError;

View File

@@ -13,7 +13,6 @@ import { RawCoMap } from "./coMap.js";
import { RawGroup, InviteSecret } from "./group.js";
import { LocalNode } from "../index.js";
import { JsonObject } from "../jsonValue.js";
import { err, ok, Result } from "neverthrow";
export function accountHeaderForInitialAgentSecret(
agentSecret: AgentSecret,
@@ -31,34 +30,28 @@ export function accountHeaderForInitialAgentSecret(
};
}
export type InvalidAccountAgentIDError = {
type: "InvalidAccountAgentID";
reason: string;
};
export class RawAccount<
Meta extends AccountMeta = AccountMeta,
> extends RawGroup<Meta> {
_cachedCurrentAgentID: AgentID | undefined;
currentAgentID(): Result<AgentID, InvalidAccountAgentIDError> {
currentAgentID(): AgentID {
if (this._cachedCurrentAgentID) {
return ok(this._cachedCurrentAgentID);
return this._cachedCurrentAgentID;
}
const agents = this.keys().filter((k): k is AgentID =>
k.startsWith("sealer_"),
);
if (agents.length !== 1) {
return err({
type: "InvalidAccountAgentID",
reason: "Account has " + agents.length + " agents",
});
throw new Error(
"Expected exactly one agent in account, got " + agents.length,
);
}
this._cachedCurrentAgentID = agents[0];
return ok(agents[0]!);
return agents[0]!;
}
}
@@ -66,10 +59,10 @@ export interface ControlledAccountOrAgent {
id: AccountID | AgentID;
agentSecret: AgentSecret;
currentAgentID: () => Result<AgentID, InvalidAccountAgentIDError>;
currentSignerID: () => Result<SignerID, InvalidAccountAgentIDError>;
currentAgentID: () => AgentID;
currentSignerID: () => SignerID;
currentSignerSecret: () => SignerSecret;
currentSealerID: () => Result<SealerID, InvalidAccountAgentIDError>;
currentSealerID: () => SealerID;
currentSealerSecret: () => SealerSecret;
}
@@ -103,29 +96,25 @@ export class RawControlledAccount<Meta extends AccountMeta = AccountMeta>
return this.core.node.acceptInvite(groupOrOwnedValueID, inviteSecret);
}
currentAgentID(): Result<AgentID, InvalidAccountAgentIDError> {
currentAgentID(): AgentID {
if (this._cachedCurrentAgentID) {
return ok(this._cachedCurrentAgentID);
return this._cachedCurrentAgentID;
}
const agentID = this.crypto.getAgentID(this.agentSecret);
this._cachedCurrentAgentID = agentID;
return ok(agentID);
return agentID;
}
currentSignerID() {
return this.currentAgentID().map((id) =>
this.crypto.getAgentSignerID(id),
);
currentSignerID(): SignerID {
return this.crypto.getAgentSignerID(this.currentAgentID());
}
currentSignerSecret(): SignerSecret {
return this.crypto.getAgentSignerSecret(this.agentSecret);
}
currentSealerID() {
return this.currentAgentID().map((id) =>
this.crypto.getAgentSealerID(id),
);
currentSealerID(): SealerID {
return this.crypto.getAgentSealerID(this.currentAgentID());
}
currentSealerSecret(): SealerSecret {
@@ -144,24 +133,20 @@ export class ControlledAgent implements ControlledAccountOrAgent {
return this.crypto.getAgentID(this.agentSecret);
}
currentAgentID() {
return ok(this.crypto.getAgentID(this.agentSecret));
currentAgentID(): AgentID {
return this.crypto.getAgentID(this.agentSecret);
}
currentSignerID() {
return this.currentAgentID().map((id) =>
this.crypto.getAgentSignerID(id),
);
currentSignerID(): SignerID {
return this.crypto.getAgentSignerID(this.currentAgentID());
}
currentSignerSecret(): SignerSecret {
return this.crypto.getAgentSignerSecret(this.agentSecret);
}
currentSealerID() {
return this.currentAgentID().map((id) =>
this.crypto.getAgentSealerID(id),
);
currentSealerID(): SealerID {
return this.crypto.getAgentSealerID(this.currentAgentID());
}
currentSealerSecret(): SealerSecret {

View File

@@ -120,9 +120,7 @@ export class RawGroup<
const agent =
typeof account === "string"
? account
: account
.currentAgentID()
._unsafeUnwrap({ withStackTrace: true });
: account.currentAgentID();
this.set(memberKey, role, "trusting");
if (this.get(memberKey) !== role) {
@@ -177,7 +175,7 @@ export class RawGroup<
const reader = this.core.node.resolveAccountAgent(
readerID,
"Expected to know currently permitted reader",
)._unsafeUnwrap({ withStackTrace: true });
);
this.set(
`${newReadKey.id}_for_${readerID}`,

View File

@@ -18,7 +18,7 @@ import {
} from "./crypto/crypto.js";
import { WasmCrypto } from "./crypto/WasmCrypto.js";
import { PureJSCrypto } from "./crypto/PureJSCrypto.js";
import { connectedPeers, Channel } from "./streamUtils.js";
import { connectedPeers } from "./streamUtils.js";
import { ControlledAgent, RawControlledAccount } from "./coValues/account.js";
import type { Role } from "./permissions.js";
import { rawCoIDtoBytes, rawCoIDfromBytes, isRawCoID } from "./ids.js";
@@ -59,7 +59,12 @@ import type * as Media from "./media.js";
type Value = JsonValue | AnyRawCoValue;
import { LSMStorage, BlockFilename, WalFilename } from "./storage/index.js";
import {
LSMStorage,
FSErr,
BlockFilename,
WalFilename,
} from "./storage/index.js";
import { FileSystem } from "./storage/FileSystem.js";
/** @hidden */
@@ -79,7 +84,6 @@ export const cojsonInternals = {
accountHeaderForInitialAgentSecret,
idforHeader,
StreamingHash,
Channel,
};
export {
@@ -119,17 +123,18 @@ export {
SyncMessage,
isRawCoID,
LSMStorage,
DisconnectedError,
PingTimeoutError,
};
export type {
Value,
FileSystem,
FSErr,
BlockFilename,
WalFilename,
IncomingSyncStream,
OutgoingSyncQueue,
DisconnectedError,
PingTimeoutError,
};
// eslint-disable-next-line @typescript-eslint/no-namespace

View File

@@ -22,11 +22,9 @@ import {
AccountID,
RawProfile,
RawAccountMigration,
InvalidAccountAgentIDError,
} from "./coValues/account.js";
import { Profile, RawCoValue } from "./index.js";
import { expectGroup } from "./typeUtils/expectGroup.js";
import { err, ok, okAsync, Result, ResultAsync } from "neverthrow";
/** A `LocalNode` represents a local view of a set of loaded `CoValue`s, from the perspective of a particular account (or primitive cryptographic agent).
@@ -51,8 +49,6 @@ export class LocalNode {
/** @category 3. Low-level */
syncManager = new SyncManager(this);
crashed: Error | undefined = undefined;
/** @category 3. Low-level */
constructor(
account: ControlledAccountOrAgent,
@@ -244,12 +240,6 @@ export class LocalNode {
/** @internal */
createCoValue(header: CoValueHeader): CoValueCore {
if (this.crashed) {
throw new Error("Trying to create CoValue after node has crashed", {
cause: this.crashed,
});
}
const coValue = new CoValueCore(header, this);
this.coValues[coValue.id] = { state: "loaded", coValue: coValue };
@@ -267,12 +257,6 @@ export class LocalNode {
onProgress?: (progress: number) => void;
} = {},
): Promise<CoValueCore | "unavailable"> {
if (this.crashed) {
throw new Error("Trying to load CoValue after node has crashed", {
cause: this.crashed,
});
}
let entry = this.coValues[id];
if (!entry) {
const peersToWaitFor = new Set(
@@ -535,9 +519,9 @@ export class LocalNode {
resolveAccountAgent(
id: AccountID | AgentID,
expectation?: string,
): Result<AgentID, ResolveAccountAgentError> {
): AgentID {
if (isAgentID(id)) {
return ok(id);
return id;
}
const coValue = this.expectCoValueLoaded(id, expectation);
@@ -549,58 +533,49 @@ export class LocalNode {
!("type" in coValue.header.meta) ||
coValue.header.meta.type !== "account"
) {
return err({
type: "UnexpectedlyNotAccount",
expectation,
id,
} satisfies UnexpectedlyNotAccountError);
throw new Error(
`${
expectation ? expectation + ": " : ""
}CoValue ${id} is not an account`,
);
}
return (coValue.getCurrentContent() as RawAccount).currentAgentID();
}
resolveAccountAgentAsync(
async resolveAccountAgentAsync(
id: AccountID | AgentID,
expectation?: string,
): ResultAsync<AgentID, ResolveAccountAgentError> {
): Promise<AgentID> {
if (isAgentID(id)) {
return okAsync(id);
return id;
}
return ResultAsync.fromPromise(
this.loadCoValueCore(id),
(e) =>
({
type: "ErrorLoadingCoValueCore",
expectation,
id,
error: e,
}) satisfies LoadCoValueCoreError,
).andThen((coValue) => {
if (coValue === "unavailable") {
return err({
type: "AccountUnavailableFromAllPeers" as const,
expectation,
id,
} satisfies AccountUnavailableFromAllPeersError);
}
const coValue = await this.loadCoValueCore(id);
if (
coValue.header.type !== "comap" ||
coValue.header.ruleset.type !== "group" ||
!coValue.header.meta ||
!("type" in coValue.header.meta) ||
coValue.header.meta.type !== "account"
) {
return err({
type: "UnexpectedlyNotAccount" as const,
expectation,
id,
} satisfies UnexpectedlyNotAccountError);
}
if (coValue === "unavailable") {
throw new Error(
`${
expectation ? expectation + ": " : ""
}Account ${id} is unavailable from all peers`,
);
}
return (coValue.getCurrentContent() as RawAccount).currentAgentID();
});
if (
coValue.header.type !== "comap" ||
coValue.header.ruleset.type !== "group" ||
!coValue.header.meta ||
!("type" in coValue.header.meta) ||
coValue.header.meta.type !== "account"
) {
throw new Error(
`${
expectation ? expectation + ": " : ""
}CoValue ${id} is not an account`,
);
}
return (coValue.getCurrentContent() as RawAccount).currentAgentID();
}
/**
@@ -625,9 +600,7 @@ export class LocalNode {
this.crypto.seal({
message: readKey.secret,
from: this.account.currentSealerSecret(),
to: this.account
.currentSealerID()
._unsafeUnwrap({ withStackTrace: true }),
to: this.account.currentSealerID(),
nOnceMaterial: {
in: groupCoValue.id,
tx: groupCoValue.nextTransactionID(),
@@ -697,10 +670,6 @@ export class LocalNode {
return newNode;
}
gracefulShutdown() {
this.syncManager.gracefulShutdown();
}
}
/** @internal */
@@ -727,31 +696,6 @@ type CoValueState =
onProgress?: (progress: number) => void;
};
export type LoadCoValueCoreError = {
type: "ErrorLoadingCoValueCore";
error: unknown;
expectation?: string;
id: AccountID;
};
export type AccountUnavailableFromAllPeersError = {
type: "AccountUnavailableFromAllPeers";
expectation?: string;
id: AccountID;
};
export type UnexpectedlyNotAccountError = {
type: "UnexpectedlyNotAccount";
expectation?: string;
id: AccountID;
};
export type ResolveAccountAgentError =
| InvalidAccountAgentIDError
| LoadCoValueCoreError
| AccountUnavailableFromAllPeersError
| UnexpectedlyNotAccountError;
/** @internal */
export function newLoadingState(
currentPeerIds: Set<PeerID>,

View File

@@ -249,22 +249,8 @@ export function determineValidTransactions(
const effectiveTransactor =
transactor === groupContent.id &&
groupAtTime instanceof RawAccount
? groupAtTime.currentAgentID().match(
(agentID) => agentID,
(e) => {
console.error(
"Error while determining current agent ID in valid transactions",
e,
);
return undefined;
},
)
? groupAtTime.currentAgentID()
: transactor;
if (!effectiveTransactor) {
return false;
}
const transactorRoleAtTxTime =
groupAtTime.get(effectiveTransactor) ||
groupAtTime.get(EVERYONE);

View File

@@ -1,3 +1,4 @@
import { Effect } from "effect";
import { CoValueChunk } from "./index.js";
import { RawCoID } from "../ids.js";
import { CryptoProvider, StreamingHash } from "../crypto/crypto.js";
@@ -10,124 +11,150 @@ export type WalEntry = { id: RawCoID } & CoValueChunk;
export type WalFilename = `wal-${number}.jsonl`;
export type FSErr = {
type: "fileSystemError";
error: Error;
};
export interface FileSystem<WriteHandle, ReadHandle> {
crypto: CryptoProvider;
createFile(filename: string): Promise<WriteHandle>;
append(handle: WriteHandle, data: Uint8Array): Promise<void>;
close(handle: ReadHandle | WriteHandle): Promise<void>;
closeAndRename(handle: WriteHandle, filename: BlockFilename): Promise<void>;
openToRead(filename: string): Promise<{ handle: ReadHandle; size: number }>;
createFile(filename: string): Effect.Effect<WriteHandle, FSErr>;
append(handle: WriteHandle, data: Uint8Array): Effect.Effect<void, FSErr>;
close(handle: ReadHandle | WriteHandle): Effect.Effect<void, FSErr>;
closeAndRename(
handle: WriteHandle,
filename: BlockFilename,
): Effect.Effect<void, FSErr>;
openToRead(
filename: string,
): Effect.Effect<{ handle: ReadHandle; size: number }, FSErr>;
read(
handle: ReadHandle,
offset: number,
length: number,
): Promise<Uint8Array>;
listFiles(): Promise<string[]>;
removeFile(filename: BlockFilename | WalFilename): Promise<void>;
): Effect.Effect<Uint8Array, FSErr>;
listFiles(): Effect.Effect<string[], FSErr>;
removeFile(
filename: BlockFilename | WalFilename,
): Effect.Effect<void, FSErr>;
}
export const textEncoder = new TextEncoder();
export const textDecoder = new TextDecoder();
export async function readChunk<RH, FS extends FileSystem<unknown, RH>>(
export function readChunk<RH, FS extends FileSystem<unknown, RH>>(
handle: RH,
header: { start: number; length: number },
fs: FS,
): Promise<CoValueChunk> {
const chunkBytes = await fs.read(handle, header.start, header.length);
): Effect.Effect<CoValueChunk, FSErr> {
return Effect.gen(function* ($) {
const chunkBytes = yield* $(
fs.read(handle, header.start, header.length),
);
const chunk = JSON.parse(textDecoder.decode(chunkBytes));
return chunk;
const chunk = JSON.parse(textDecoder.decode(chunkBytes));
return chunk;
});
}
export async function readHeader<RH, FS extends FileSystem<unknown, RH>>(
export function readHeader<RH, FS extends FileSystem<unknown, RH>>(
filename: string,
handle: RH,
size: number,
fs: FS,
): Promise<BlockHeader> {
const headerLength = Number(filename.match(/-H(\d+)\.jsonl$/)![1]!);
): Effect.Effect<BlockHeader, FSErr> {
return Effect.gen(function* ($) {
const headerLength = Number(filename.match(/-H(\d+)\.jsonl$/)![1]!);
const headerBytes = await fs.read(
handle,
size - headerLength,
headerLength,
);
const headerBytes = yield* $(
fs.read(handle, size - headerLength, headerLength),
);
const header = JSON.parse(textDecoder.decode(headerBytes));
return header;
const header = JSON.parse(textDecoder.decode(headerBytes));
return header;
});
}
export async function writeBlock<WH, RH, FS extends FileSystem<WH, RH>>(
export function writeBlock<WH, RH, FS extends FileSystem<WH, RH>>(
chunks: Map<RawCoID, CoValueChunk>,
level: number,
blockNumber: number,
fs: FS,
): Promise<BlockFilename> {
): Effect.Effect<BlockFilename, FSErr> {
if (chunks.size === 0) {
throw new Error("No chunks to write");
return Effect.die(new Error("No chunks to write"));
}
const blockHeader: BlockHeader = [];
return Effect.gen(function* ($) {
const blockHeader: BlockHeader = [];
let offset = 0;
let offset = 0;
const file = await fs.createFile(
"wipBlock" + Math.random().toString(36).substring(7) + ".tmp.jsonl",
);
const hash = new StreamingHash(fs.crypto);
const file = yield* $(
fs.createFile(
"wipBlock" +
Math.random().toString(36).substring(7) +
".tmp.jsonl",
),
);
const hash = new StreamingHash(fs.crypto);
const chunksSortedById = Array.from(chunks).sort(([id1], [id2]) =>
id1.localeCompare(id2),
);
const chunksSortedById = Array.from(chunks).sort(([id1], [id2]) =>
id1.localeCompare(id2),
);
for (const [id, chunk] of chunksSortedById) {
const encodedBytes = hash.update(chunk);
const encodedBytesWithNewline = new Uint8Array(encodedBytes.length + 1);
encodedBytesWithNewline.set(encodedBytes);
encodedBytesWithNewline[encodedBytes.length] = 10;
await fs.append(file, encodedBytesWithNewline);
const length = encodedBytesWithNewline.length;
blockHeader.push({ id, start: offset, length });
offset += length;
}
for (const [id, chunk] of chunksSortedById) {
const encodedBytes = hash.update(chunk);
const encodedBytesWithNewline = new Uint8Array(
encodedBytes.length + 1,
);
encodedBytesWithNewline.set(encodedBytes);
encodedBytesWithNewline[encodedBytes.length] = 10;
yield* $(fs.append(file, encodedBytesWithNewline));
const length = encodedBytesWithNewline.length;
blockHeader.push({ id, start: offset, length });
offset += length;
}
const headerBytes = textEncoder.encode(JSON.stringify(blockHeader));
await fs.append(file, headerBytes);
const headerBytes = textEncoder.encode(JSON.stringify(blockHeader));
yield* $(fs.append(file, headerBytes));
// console.log(
// "full file",
// yield* $(
// fs.read(file as unknown as RH, 0, offset + headerBytes.length),
// ),
// );
// console.log(
// "full file",
// yield* $(
// fs.read(file as unknown as RH, 0, offset + headerBytes.length),
// ),
// );
const filename: BlockFilename = `L${level}-${(blockNumber + "").padStart(
3,
"0",
)}-${hash.digest().replace("hash_", "").slice(0, 15)}-H${
headerBytes.length
}.jsonl`;
// console.log("renaming to" + filename);
await fs.closeAndRename(file, filename);
const filename: BlockFilename = `L${level}-${(
blockNumber + ""
).padStart(3, "0")}-${hash
.digest()
.replace("hash_", "")
.slice(0, 15)}-H${headerBytes.length}.jsonl`;
// console.log("renaming to" + filename);
yield* $(fs.closeAndRename(file, filename));
return filename;
return filename;
// console.log("Wrote block", filename, blockHeader);
// console.log("IDs in block", blockHeader.map(e => e.id));
// console.log("Wrote block", filename, blockHeader);
// console.log("IDs in block", blockHeader.map(e => e.id));
});
}
export async function writeToWal<WH, RH, FS extends FileSystem<WH, RH>>(
export function writeToWal<WH, RH, FS extends FileSystem<WH, RH>>(
handle: WH,
fs: FS,
id: RawCoID,
chunk: CoValueChunk,
) {
const walEntry: WalEntry = {
id,
...chunk,
};
const bytes = textEncoder.encode(JSON.stringify(walEntry) + "\n");
console.log("writing to WAL", handle, id, bytes.length);
return fs.append(handle, bytes);
): Effect.Effect<void, FSErr> {
return Effect.gen(function* ($) {
const walEntry: WalEntry = {
id,
...chunk,
};
const bytes = textEncoder.encode(JSON.stringify(walEntry) + "\n");
console.log("writing to WAL", handle, id, bytes.length);
yield* $(fs.append(handle, bytes));
});
}

View File

@@ -1,3 +1,4 @@
import { Either } from "effect";
import { RawCoID, SessionID } from "../ids.js";
import { MAX_RECOMMENDED_TX_SIZE } from "../index.js";
import { CoValueKnownState, NewContentMessage } from "../sync.js";
@@ -79,7 +80,7 @@ export function chunkToKnownState(id: RawCoID, chunk: CoValueChunk) {
export function mergeChunks(
chunkA: CoValueChunk,
chunkB: CoValueChunk,
): "nonContigous" | CoValueChunk {
): Either.Either<"nonContigous", CoValueChunk> {
const header = chunkA.header || chunkB.header;
const newSessions = { ...chunkA.sessionEntries };
@@ -132,9 +133,9 @@ export function mergeChunks(
}
newSessions[sessionID] = newEntries;
} else {
return "nonContigous" as const;
return Either.right("nonContigous" as const);
}
}
return { header, sessionEntries: newSessions };
return Either.left({ header, sessionEntries: newSessions });
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
import { Console, Effect, Queue, Stream } from "effect";
import { Peer, PeerID, SyncMessage } from "./sync.js";
import { Channel } from "queueable";
export { Channel } from "queueable";
export function connectedPeers(
peer1id: PeerID,
@@ -9,66 +8,65 @@ export function connectedPeers(
trace = false,
peer1role = "peer",
peer2role = "peer",
crashOnClose = false,
}: {
trace?: boolean;
peer1role?: Peer["role"];
peer2role?: Peer["role"];
crashOnClose?: boolean;
} = {},
): [Peer, Peer] {
const [from1to2Rx, from1to2Tx] = newQueuePair(
trace ? { traceAs: `${peer1id} -> ${peer2id}` } : undefined,
);
const [from2to1Rx, from2to1Tx] = newQueuePair(
trace ? { traceAs: `${peer2id} -> ${peer1id}` } : undefined,
);
): Effect.Effect<[Peer, Peer]> {
return Effect.gen(function* () {
const [from1to2Rx, from1to2Tx] = yield* newQueuePair(
trace ? { traceAs: `${peer1id} -> ${peer2id}` } : undefined,
);
const [from2to1Rx, from2to1Tx] = yield* newQueuePair(
trace ? { traceAs: `${peer2id} -> ${peer1id}` } : undefined,
);
const peer2AsPeer: Peer = {
id: peer2id,
incoming: from2to1Rx,
outgoing: from1to2Tx,
role: peer2role,
crashOnClose: crashOnClose,
};
const peer2AsPeer: Peer = {
id: peer2id,
incoming: from2to1Rx,
outgoing: from1to2Tx,
role: peer2role,
};
const peer1AsPeer: Peer = {
id: peer1id,
incoming: from1to2Rx,
outgoing: from2to1Tx,
role: peer1role,
crashOnClose: crashOnClose,
};
const peer1AsPeer: Peer = {
id: peer1id,
incoming: from1to2Rx,
outgoing: from2to1Tx,
role: peer1role,
};
return [peer1AsPeer, peer2AsPeer];
return [peer1AsPeer, peer2AsPeer];
});
}
export function newQueuePair(
options: { traceAs?: string } = {},
): [AsyncIterable<SyncMessage>, Channel<SyncMessage>] {
const channel = new Channel<SyncMessage>();
): Effect.Effect<[Stream.Stream<SyncMessage>, Queue.Enqueue<SyncMessage>]> {
return Effect.gen(function* () {
const queue = yield* Queue.unbounded<SyncMessage>();
if (options.traceAs) {
return [
(async function* () {
for await (const msg of channel) {
console.debug(
options.traceAs,
JSON.stringify(
msg,
(k, v) =>
k === "changes" || k === "encryptedChanges"
? v.slice(0, 20) + "..."
: v,
2,
if (options.traceAs) {
return [
Stream.fromQueue(queue).pipe(
Stream.tap((msg) =>
Console.debug(
options.traceAs,
JSON.stringify(
msg,
(k, v) =>
k === "changes" || k === "encryptedChanges"
? v.slice(0, 20) + "..."
: v,
2,
),
),
);
yield msg;
}
})(),
channel,
];
} else {
return [channel.wrap(), channel];
}
),
),
queue,
];
} else {
return [Stream.fromQueue(queue), queue];
}
});
}

View File

@@ -3,6 +3,7 @@ import { CoValueHeader, Transaction } from "./coValueCore.js";
import { CoValueCore } from "./coValueCore.js";
import { LocalNode, newLoadingState } from "./localNode.js";
import { RawCoID, SessionID } from "./ids.js";
import { Data, Effect, Queue, Stream } from "effect";
export type CoValueKnownState = {
id: RawCoID;
@@ -55,25 +56,27 @@ export type DoneMessage = {
export type PeerID = string;
export type DisconnectedError = "Disconnected";
export class DisconnectedError extends Data.TaggedError("DisconnectedError")<{
message: string;
}> {}
export type PingTimeoutError = "PingTimeout";
export class PingTimeoutError extends Error {
readonly _tag = "PingTimeoutError";
}
export type IncomingSyncStream = AsyncIterable<
SyncMessage | DisconnectedError | PingTimeoutError
export type IncomingSyncStream = Stream.Stream<
SyncMessage,
DisconnectedError | PingTimeoutError
>;
export type OutgoingSyncQueue = {
push: (msg: SyncMessage) => Promise<unknown>;
close: () => void;
};
export type OutgoingSyncQueue = Queue.Enqueue<SyncMessage>;
export interface Peer {
id: PeerID;
incoming: IncomingSyncStream;
outgoing: OutgoingSyncQueue;
role: "peer" | "server" | "client";
delayOnError?: number;
priority?: number;
crashOnClose: boolean;
}
export interface PeerState {
@@ -83,8 +86,8 @@ export interface PeerState {
incoming: IncomingSyncStream;
outgoing: OutgoingSyncQueue;
role: "peer" | "server" | "client";
delayOnError?: number;
priority?: number;
crashOnClose: boolean;
}
export function combinedKnownStates(
@@ -141,11 +144,15 @@ export class SyncManager {
for (const peer of eligiblePeers) {
// console.log("loading", id, "from", peer.id);
await peer.outgoing.push({
action: "load",
id: id,
header: false,
sessions: {},
Effect.runPromise(
Queue.offer(peer.outgoing, {
action: "load",
id: id,
header: false,
sessions: {},
}),
).catch((e) => {
console.error("Error writing to peer", e);
});
const coValueEntry = this.local.coValues[id];
@@ -222,13 +229,11 @@ export class SyncManager {
}
if (entry.state === "loading") {
this.trySendToPeer(peer, {
await this.trySendToPeer(peer, {
action: "load",
id,
header: false,
sessions: {},
}).catch((e) => {
console.error("Error sending load", e);
});
return;
}
@@ -241,11 +246,9 @@ export class SyncManager {
if (!peer.toldKnownState.has(id)) {
peer.toldKnownState.add(id);
this.trySendToPeer(peer, {
await this.trySendToPeer(peer, {
action: "load",
...coValue.knownState(),
}).catch((e) => {
console.error("Error sending load", e);
});
}
}
@@ -270,12 +273,10 @@ export class SyncManager {
);
if (!peer.toldKnownState.has(id)) {
this.trySendToPeer(peer, {
await this.trySendToPeer(peer, {
action: "known",
asDependencyOf,
...coValue.knownState(),
}).catch((e) => {
console.error("Error sending known state", e);
});
peer.toldKnownState.add(id);
@@ -310,9 +311,7 @@ export class SyncManager {
// } header: ${!!piece.header}`,
// // Object.values(piece.new).map((s) => s.newTransactions)
// );
this.trySendToPeer(peer, piece).catch((e) => {
console.error("Error sending content piece", e);
});
await this.trySendToPeer(peer, piece);
if (performance.now() - lastYield > 10) {
await new Promise<void>((resolve) => {
setTimeout(resolve, 0);
@@ -343,8 +342,8 @@ export class SyncManager {
outgoing: peer.outgoing,
toldKnownState: new Set(),
role: peer.role,
delayOnError: peer.delayOnError,
priority: peer.priority,
crashOnClose: peer.crashOnClose,
};
this.peers[peer.id] = peerState;
@@ -366,61 +365,55 @@ export class SyncManager {
void initialSync();
}
const processMessages = async () => {
for await (const msg of peerState.incoming) {
if (msg === "Disconnected") {
return;
}
if (msg === "PingTimeout") {
console.error("Ping timeout from peer", peer.id);
return;
}
try {
await this.handleSyncMessage(msg, peerState);
} catch (e) {
throw new Error(
`Error reading from peer ${
peer.id
}, handling msg\n\n${JSON.stringify(msg, (k, v) =>
k === "changes" || k === "encryptedChanges"
? v.slice(0, 20) + "..."
: v,
)}`,
{ cause: e },
);
}
}
};
processMessages()
.then(() => {
if (peer.crashOnClose) {
console.error("Unexepcted close from peer", peer.id);
this.local.crashed = new Error(
"Unexpected close from peer",
);
throw new Error("Unexpected close from peer");
}
})
.catch((e) => {
console.error(
"Error processing messages from peer",
peer.id,
e,
);
if (peer.crashOnClose) {
this.local.crashed = e;
throw new Error(e);
}
})
.finally(() => {
peer.outgoing.close();
delete this.peers[peer.id];
});
void Effect.runPromise(
peerState.incoming.pipe(
Stream.ensuring(
Effect.sync(() => {
console.log("Peer disconnected:", peer.id);
delete this.peers[peer.id];
}),
),
Stream.runForEach((msg) =>
Effect.tryPromise({
try: () => this.handleSyncMessage(msg, peerState),
catch: (e) =>
new Error(
`Error reading from peer ${
peer.id
}, handling msg\n\n${JSON.stringify(
msg,
(k, v) =>
k === "changes" ||
k === "encryptedChanges"
? v.slice(0, 20) + "..."
: v,
)}`,
{ cause: e },
),
}).pipe(
Effect.timeoutFail({
duration: 10000,
onTimeout: () =>
new Error("Took >10s to process message"),
}),
),
),
Effect.catchAll((e) =>
Effect.logError(
"Error in peer",
peer.id,
e.message,
typeof e.cause === "object" &&
e.cause instanceof Error &&
e.cause.message,
),
),
),
);
}
trySendToPeer(peer: PeerState, msg: SyncMessage) {
return peer.outgoing.push(msg);
return Effect.runPromise(Queue.offer(peer.outgoing, msg));
}
async handleLoad(msg: LoadMessage, peer: PeerState) {
@@ -433,7 +426,7 @@ export class SyncManager {
// special case: we should be able to solve this much more neatly
// with an explicit state machine in the future
const eligiblePeers = this.peersInPriorityOrder().filter(
(other) => other.id !== peer.id && other.role === "server",
(other) => other.id !== peer.id && peer.role === "server",
);
if (eligiblePeers.length === 0) {
if (msg.header || Object.keys(msg.sessions).length > 0) {
@@ -446,7 +439,7 @@ export class SyncManager {
header: false,
sessions: {},
}).catch((e) => {
console.error("Error sending known state", e);
console.error("Error sending known state back", e);
});
}
return;
@@ -465,25 +458,23 @@ export class SyncManager {
}
if (entry.state === "loading") {
// console.debug(
// "Waiting for loaded",
// msg.id,
// "after message from",
// peer.id,
// );
console.log(
"Waiting for loaded",
msg.id,
"after message from",
peer.id,
);
const loaded = await entry.done;
// console.log("Loaded", msg.id, loaded);
console.log("Loaded", msg.id, loaded);
if (loaded === "unavailable") {
peer.optimisticKnownStates[msg.id] = knownStateIn(msg);
peer.toldKnownState.add(msg.id);
this.trySendToPeer(peer, {
await this.trySendToPeer(peer, {
action: "known",
id: msg.id,
header: false,
sessions: {},
}).catch((e) => {
console.error("Error sending known state back", e);
});
return;
@@ -561,10 +552,9 @@ export class SyncManager {
let entry = this.local.coValues[msg.id];
if (!entry) {
console.error(
throw new Error(
`Expected coValue entry for ${msg.id} to be created on new content, missing subscribe?`,
);
return;
}
let resolveAfterDone: ((coValue: CoValueCore) => void) | undefined;
@@ -572,16 +562,14 @@ export class SyncManager {
const peerOptimisticKnownState = peer.optimisticKnownStates[msg.id];
if (!peerOptimisticKnownState) {
console.error(
throw new Error(
"Expected optimisticKnownState to be set for coValue we receive new content for",
);
return;
}
if (entry.state === "loading") {
if (!msg.header) {
console.error("Expected header to be sent in first message");
return;
throw new Error("Expected header to be sent in first message");
}
const firstPeerStateEntry = entry.firstPeerState[peer.id];
@@ -633,8 +621,7 @@ export class SyncManager {
}
const before = performance.now();
// eslint-disable-next-line neverthrow/must-use-result
const result = await coValue.tryAddTransactionsAsync(
const success = await coValue.tryAddTransactionsAsync(
sessionID,
newTransactions,
undefined,
@@ -670,16 +657,15 @@ export class SyncManager {
entry.onProgress?.(ourTotalnTxs / theirTotalnTxs);
if (result.isErr()) {
if (!success) {
console.error(
"Failed to add transactions from",
peer.id,
result.error,
"Failed to add transactions",
msg.id,
newTransactions.length + " new transactions",
"after: " + newContentForSession.after,
"our last known tx idx initially: " + ourKnownTxIdx,
"our last known tx idx now: " + coValue.sessionLogs.get(sessionID)?.transactions.length,
JSON.stringify(newTransactions, (k, v) =>
k === "changes" || k === "encryptedChanges"
? v.slice(0, 20) + "..."
: v,
),
);
continue;
}
@@ -698,12 +684,10 @@ export class SyncManager {
await this.syncCoValue(coValue);
if (invalidStateAssumed) {
this.trySendToPeer(peer, {
await this.trySendToPeer(peer, {
action: "known",
isCorrection: true,
...coValue.knownState(),
}).catch((e) => {
console.error("Error sending known state correction", e);
});
}
}
@@ -771,16 +755,6 @@ export class SyncManager {
}
}
}
gracefulShutdown() {
for (const peer of Object.values(this.peers)) {
console.debug("Gracefully closing", peer.id);
peer.outgoing.close();
peer.incoming = (async function* () {
yield "Disconnected" as const;
})();
}
}
}
function knownStateIn(msg: LoadMessage | KnownStateMessage) {

View File

@@ -3,6 +3,7 @@ import { newRandomSessionID } from "../coValueCore.js";
import { LocalNode } from "../localNode.js";
import { connectedPeers } from "../streamUtils.js";
import { WasmCrypto } from "../crypto/WasmCrypto.js";
import { Effect } from "effect";
const Crypto = await WasmCrypto.create();
@@ -52,11 +53,13 @@ test("Can create account with one node, and then load it on another", async () =
map.set("foo", "bar", "private");
expect(map.get("foo")).toEqual("bar");
const [node1asPeer, node2asPeer] = connectedPeers("node1", "node2", {
trace: true,
peer1role: "server",
peer2role: "client",
})
const [node1asPeer, node2asPeer] = await Effect.runPromise(
connectedPeers("node1", "node2", {
trace: true,
peer1role: "server",
peer2role: "client",
}),
);
console.log("After connected peers");

View File

@@ -36,14 +36,12 @@ test("Can create coValue with new agent credentials and add transaction to it",
);
expect(
coValue
.tryAddTransactions(
node.currentSessionID,
[transaction],
expectedNewHash,
Crypto.sign(account.currentSignerSecret(), expectedNewHash),
)
._unsafeUnwrap(),
coValue.tryAddTransactions(
node.currentSessionID,
[transaction],
expectedNewHash,
Crypto.sign(account.currentSignerSecret(), expectedNewHash),
),
).toBe(true);
});
@@ -74,9 +72,8 @@ test("transactions with wrong signature are rejected", () => {
[transaction],
);
// eslint-disable-next-line neverthrow/must-use-result
coValue
.tryAddTransactions(
expect(
coValue.tryAddTransactions(
node.currentSessionID,
[transaction],
expectedNewHash,
@@ -84,8 +81,8 @@ test("transactions with wrong signature are rejected", () => {
Crypto.getAgentSignerSecret(wrongAgent),
expectedNewHash,
),
)
._unsafeUnwrapErr({ withStackTrace: true });
),
).toBe(false);
});
test("transactions with correctly signed, but wrong hash are rejected", () => {
@@ -124,15 +121,14 @@ test("transactions with correctly signed, but wrong hash are rejected", () => {
],
);
// eslint-disable-next-line neverthrow/must-use-result
coValue
.tryAddTransactions(
expect(
coValue.tryAddTransactions(
node.currentSessionID,
[transaction],
expectedNewHash,
Crypto.sign(account.currentSignerSecret(), expectedNewHash),
)
._unsafeUnwrapErr({ withStackTrace: true });
),
).toBe(false);
});
test("New transactions in a group correctly update owned values, including subscriptions", async () => {
@@ -178,14 +174,12 @@ test("New transactions in a group correctly update owned values, including subsc
expect(map.core.getValidSortedTransactions().length).toBe(1);
const manuallyAdddedTxSuccess = group.core
.tryAddTransactions(
node.currentSessionID,
[resignationThatWeJustLearnedAbout],
expectedNewHash,
signature,
)
._unsafeUnwrap({ withStackTrace: true });
const manuallyAdddedTxSuccess = group.core.tryAddTransactions(
node.currentSessionID,
[resignationThatWeJustLearnedAbout],
expectedNewHash,
signature,
);
expect(manuallyAdddedTxSuccess).toBe(true);

View File

@@ -354,7 +354,7 @@ test("Admins can set group read key and then use it to create and read private t
const revelation = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -410,7 +410,7 @@ test("Admins can set group read key and then writers can use it to create and re
const revelation1 = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -422,7 +422,7 @@ test("Admins can set group read key and then writers can use it to create and re
const revelation2 = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: writer.currentSealerID()._unsafeUnwrap(),
to: writer.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -489,7 +489,7 @@ test("Admins can set group read key and then use it to create private transactio
const revelation1 = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -501,7 +501,7 @@ test("Admins can set group read key and then use it to create private transactio
const revelation2 = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: reader.currentSealerID()._unsafeUnwrap(),
to: reader.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -576,7 +576,7 @@ test("Admins can set group read key and then use it to create private transactio
const revelation1 = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -588,7 +588,7 @@ test("Admins can set group read key and then use it to create private transactio
const revelation2 = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: reader1.currentSealerID()._unsafeUnwrap(),
to: reader1.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -627,7 +627,7 @@ test("Admins can set group read key and then use it to create private transactio
const revelation3 = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: reader2.currentSealerID()._unsafeUnwrap(),
to: reader2.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -692,7 +692,7 @@ test("Admins can set group read key, make a private transaction in an owned obje
const revelation1 = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -722,7 +722,7 @@ test("Admins can set group read key, make a private transaction in an owned obje
const revelation2 = Crypto.seal({
message: readKey2,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -775,7 +775,7 @@ test("Admins can set group read key, make a private transaction in an owned obje
const revelation = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -802,7 +802,7 @@ test("Admins can set group read key, make a private transaction in an owned obje
const revelation2 = Crypto.seal({
message: readKey2,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -814,7 +814,7 @@ test("Admins can set group read key, make a private transaction in an owned obje
const revelation3 = Crypto.seal({
message: readKey2,
from: admin.currentSealerSecret(),
to: reader.currentSealerID()._unsafeUnwrap(),
to: reader.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -910,7 +910,7 @@ test("Admins can set group read rey, make a private transaction in an owned obje
const revelation1 = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -922,7 +922,7 @@ test("Admins can set group read rey, make a private transaction in an owned obje
const revelation2 = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: reader.currentSealerID()._unsafeUnwrap(),
to: reader.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -934,7 +934,7 @@ test("Admins can set group read rey, make a private transaction in an owned obje
const revelation3 = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: reader2.currentSealerID()._unsafeUnwrap(),
to: reader2.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -980,7 +980,7 @@ test("Admins can set group read rey, make a private transaction in an owned obje
const newRevelation1 = Crypto.seal({
message: readKey2,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -996,7 +996,7 @@ test("Admins can set group read rey, make a private transaction in an owned obje
const newRevelation2 = Crypto.seal({
message: readKey2,
from: admin.currentSealerSecret(),
to: reader2.currentSealerID()._unsafeUnwrap(),
to: reader2.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -1121,7 +1121,7 @@ test("Admins can create an adminInvite, which can add an admin", () => {
const revelation = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -1231,7 +1231,7 @@ test("Admins can create a writerInvite, which can add a writer", () => {
const revelation = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -1334,7 +1334,7 @@ test("Admins can create a readerInvite, which can add a reader", () => {
const revelation = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -1427,7 +1427,7 @@ test("WriterInvites can not invite admins", () => {
const revelation = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -1481,7 +1481,7 @@ test("ReaderInvites can not invite admins", () => {
const revelation = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -1535,7 +1535,7 @@ test("ReaderInvites can not invite writers", () => {
const revelation = Crypto.seal({
message: readKey,
from: admin.currentSealerSecret(),
to: admin.currentSealerID()._unsafeUnwrap(),
to: admin.currentSealerID(),
nOnceMaterial: {
in: groupCore.id,
tx: groupCore.nextTransactionID(),
@@ -1610,7 +1610,7 @@ test("Can give read permission to 'everyone'", () => {
childObject
.testWithDifferentAccount(
newAccount,
newRandomSessionID(newAccount.currentAgentID()._unsafeUnwrap()),
newRandomSessionID(newAccount.currentAgentID()),
)
.getCurrentContent(),
);
@@ -1639,7 +1639,7 @@ test("Can give read permissions to 'everyone' (high-level)", async () => {
childObject.core
.testWithDifferentAccount(
new ControlledAgent(Crypto.newRandomAgentSecret(), Crypto),
newRandomSessionID(newAccount.currentAgentID()._unsafeUnwrap()),
newRandomSessionID(newAccount.currentAgentID()),
)
.getCurrentContent(),
);
@@ -1680,7 +1680,7 @@ test("Can give write permission to 'everyone'", async () => {
childObject
.testWithDifferentAccount(
newAccount,
newRandomSessionID(newAccount.currentAgentID()._unsafeUnwrap()),
newRandomSessionID(newAccount.currentAgentID()),
)
.getCurrentContent(),
);
@@ -1715,7 +1715,7 @@ test("Can give write permissions to 'everyone' (high-level)", async () => {
childObject.core
.testWithDifferentAccount(
newAccount,
newRandomSessionID(newAccount.currentAgentID()._unsafeUnwrap()),
newRandomSessionID(newAccount.currentAgentID()),
)
.getCurrentContent(),
);

File diff suppressed because it is too large Load Diff

View File

@@ -1,128 +1,5 @@
# jazz-browser-media-images
## 0.7.34-neverthrow.3
### Patch Changes
- jazz-browser@0.7.34-neverthrow.3
- jazz-tools@0.7.34-neverthrow.3
## 0.7.34-neverthrow.2
### Patch Changes
- jazz-browser@0.7.34-neverthrow.2
## 0.7.34-neverthrow.1
### Patch Changes
- jazz-browser@0.7.34-neverthrow.1
- jazz-tools@0.7.34-neverthrow.1
## 0.7.34-neverthrow.0
### Patch Changes
- jazz-browser@0.7.34-neverthrow.0
- jazz-tools@0.7.34-neverthrow.0
## 0.7.33
### Patch Changes
- jazz-browser@0.7.33
- jazz-tools@0.7.33
## 0.7.33-hotfixes.5
### Patch Changes
- jazz-browser@0.7.33-hotfixes.5
- jazz-tools@0.7.33-hotfixes.5
## 0.7.33-hotfixes.4
### Patch Changes
- jazz-browser@0.7.33-hotfixes.4
- jazz-tools@0.7.33-hotfixes.4
## 0.7.33-hotfixes.3
### Patch Changes
- jazz-browser@0.7.33-hotfixes.3
- jazz-tools@0.7.33-hotfixes.3
## 0.7.33-hotfixes.2
### Patch Changes
- jazz-browser@0.7.33-hotfixes.2
## 0.7.33-hotfixes.1
### Patch Changes
- jazz-browser@0.7.33-hotfixes.1
## 0.7.33-hotfixes.0
### Patch Changes
- jazz-browser@0.7.33-hotfixes.0
- jazz-tools@0.7.33-hotfixes.0
## 0.7.32
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.32
- jazz-browser@0.7.32
## 0.7.31
### Patch Changes
- jazz-browser@0.7.31
- jazz-tools@0.7.31
## 0.7.30
### Patch Changes
- jazz-browser@0.7.30
## 0.7.29
### Patch Changes
- jazz-browser@0.7.29
- jazz-tools@0.7.29
## 0.7.28
### Patch Changes
- jazz-browser@0.7.28
- jazz-tools@0.7.28
## 0.7.27
### Patch Changes
- jazz-browser@0.7.27
## 0.7.26
### Patch Changes
- Updated dependencies
- jazz-browser@0.7.26
- jazz-tools@0.7.26
## 0.7.25
### Patch Changes

View File

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

View File

@@ -1,174 +1,5 @@
# jazz-browser
## 0.7.34-neverthrow.3
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.3
- cojson-storage-indexeddb@0.7.34-neverthrow.3
- cojson-transport-ws@0.7.34-neverthrow.3
- jazz-tools@0.7.34-neverthrow.3
## 0.7.34-neverthrow.2
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.34-neverthrow.2
## 0.7.34-neverthrow.1
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.1
- cojson-storage-indexeddb@0.7.34-neverthrow.1
- cojson-transport-ws@0.7.34-neverthrow.1
- jazz-tools@0.7.34-neverthrow.1
## 0.7.34-neverthrow.0
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.0
- cojson-storage-indexeddb@0.7.34-neverthrow.0
- cojson-transport-ws@0.7.34-neverthrow.0
- jazz-tools@0.7.34-neverthrow.0
## 0.7.33
### Patch Changes
- Updated dependencies [fdde8db]
- Updated dependencies [b297c93]
- Updated dependencies [07fe2b9]
- Updated dependencies [3bf5127]
- Updated dependencies [a8b74ff]
- Updated dependencies [db53161]
- cojson-transport-ws@0.7.33
- cojson@0.7.33
- cojson-storage-indexeddb@0.7.33
- jazz-tools@0.7.33
## 0.7.33-hotfixes.5
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.5
- cojson-storage-indexeddb@0.7.33-hotfixes.5
- cojson-transport-ws@0.7.33-hotfixes.5
- jazz-tools@0.7.33-hotfixes.5
## 0.7.33-hotfixes.4
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.4
- cojson-storage-indexeddb@0.7.33-hotfixes.4
- cojson-transport-ws@0.7.33-hotfixes.4
- jazz-tools@0.7.33-hotfixes.4
## 0.7.33-hotfixes.3
### Patch Changes
- Updated dependencies
- cojson-storage-indexeddb@0.7.33-hotfixes.3
- cojson-transport-ws@0.7.33-hotfixes.3
- cojson@0.7.33-hotfixes.3
- jazz-tools@0.7.33-hotfixes.3
## 0.7.33-hotfixes.2
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.33-hotfixes.2
## 0.7.33-hotfixes.1
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.33-hotfixes.1
## 0.7.33-hotfixes.0
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.0
- cojson-storage-indexeddb@0.7.33-hotfixes.0
- cojson-transport-ws@0.7.33-hotfixes.0
- jazz-tools@0.7.33-hotfixes.0
## 0.7.32
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.32
## 0.7.31
### Patch Changes
- Updated dependencies
- Updated dependencies
- cojson-transport-ws@0.7.31
- cojson@0.7.31
- cojson-storage-indexeddb@0.7.31
- jazz-tools@0.7.31
## 0.7.30
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.30
## 0.7.29
### Patch Changes
- Updated dependencies
- cojson@0.7.29
- cojson-storage-indexeddb@0.7.29
- cojson-transport-ws@0.7.29
- jazz-tools@0.7.29
## 0.7.28
### Patch Changes
- Updated dependencies
- cojson@0.7.28
- cojson-storage-indexeddb@0.7.28
- cojson-transport-ws@0.7.28
- jazz-tools@0.7.28
## 0.7.27
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.27
## 0.7.26
### Patch Changes
- Remove Effect from jazz/cojson internals
- Updated dependencies
- cojson@0.7.26
- cojson-storage-indexeddb@0.7.26
- cojson-transport-ws@0.7.26
- jazz-tools@0.7.26
## 0.7.25
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-browser",
"version": "0.7.34-neverthrow.3",
"version": "0.7.25",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",
@@ -10,6 +10,7 @@
"cojson": "workspace:*",
"cojson-storage-indexeddb": "workspace:*",
"cojson-transport-ws": "workspace:*",
"effect": "^3.5.2",
"jazz-tools": "workspace:*",
"typescript": "^5.1.6"
},

View File

@@ -1,12 +1,13 @@
import { BlockFilename, FileSystem, WalFilename, CryptoProvider } from "cojson";
import {
BlockFilename,
FSErr,
FileSystem,
WalFilename,
CryptoProvider,
} from "cojson";
import { Effect } from "effect";
export class OPFSFilesystem
implements
FileSystem<
{ id: number; filename: string },
{ id: number; filename: string }
>
{
export class OPFSFilesystem implements FileSystem<{id: number, filename: string}, {id: number, filename: string}> {
opfsWorker: Worker;
callbacks: Map<number, (event: MessageEvent) => void> = new Map();
nextRequestId = 0;
@@ -27,8 +28,8 @@ export class OPFSFilesystem
};
}
listFiles(): Promise<string[]> {
return new Promise((resolve) => {
listFiles(): Effect.Effect<string[], FSErr, never> {
return Effect.async((cb) => {
const requestId = this.nextRequestId++;
performance.mark("listFiles" + requestId + "_listFiles");
this.callbacks.set(requestId, (event) => {
@@ -38,7 +39,7 @@ export class OPFSFilesystem
"listFiles" + requestId + "_listFiles",
"listFilesEnd" + requestId + "_listFiles",
);
resolve(event.data.fileNames);
cb(Effect.succeed(event.data.fileNames));
});
this.opfsWorker.postMessage({ type: "listFiles", requestId });
});
@@ -46,15 +47,17 @@ export class OPFSFilesystem
openToRead(
filename: string,
): Promise<{ handle: { id: number; filename: string }; size: number }> {
return new Promise((resolve) => {
): Effect.Effect<{ handle: {id: number, filename: string}; size: number }, FSErr, never> {
return Effect.async((cb) => {
const requestId = this.nextRequestId++;
performance.mark("openToRead" + "_" + filename);
this.callbacks.set(requestId, (event) => {
resolve({
handle: { id: event.data.handle, filename },
size: event.data.size,
});
cb(
Effect.succeed({
handle: {id: event.data.handle, filename},
size: event.data.size,
}),
);
performance.mark("openToReadEnd" + "_" + filename);
performance.measure(
"openToRead" + "_" + filename,
@@ -70,8 +73,8 @@ export class OPFSFilesystem
});
}
createFile(filename: string): Promise<{ id: number; filename: string }> {
return new Promise((resolve) => {
createFile(filename: string): Effect.Effect<{id: number, filename: string}, FSErr, never> {
return Effect.async((cb) => {
const requestId = this.nextRequestId++;
performance.mark("createFile" + "_" + filename);
this.callbacks.set(requestId, (event) => {
@@ -81,7 +84,7 @@ export class OPFSFilesystem
"createFile" + "_" + filename,
"createFileEnd" + "_" + filename,
);
resolve({ id: event.data.handle, filename });
cb(Effect.succeed({id: event.data.handle, filename}));
});
this.opfsWorker.postMessage({
type: "createFile",
@@ -91,8 +94,10 @@ export class OPFSFilesystem
});
}
openToWrite(filename: string): Promise<{ id: number; filename: string }> {
return new Promise((resolve) => {
openToWrite(
filename: string,
): Effect.Effect<{id: number, filename: string}, FSErr, never> {
return Effect.async((cb) => {
const requestId = this.nextRequestId++;
performance.mark("openToWrite" + "_" + filename);
this.callbacks.set(requestId, (event) => {
@@ -102,7 +107,7 @@ export class OPFSFilesystem
"openToWrite" + "_" + filename,
"openToWriteEnd" + "_" + filename,
);
resolve({ id: event.data.handle, filename });
cb(Effect.succeed({id: event.data.handle, filename}));
});
this.opfsWorker.postMessage({
type: "openToWrite",
@@ -113,10 +118,10 @@ export class OPFSFilesystem
}
append(
handle: { id: number; filename: string },
handle: {id: number, filename: string},
data: Uint8Array,
): Promise<void> {
return new Promise((resolve) => {
): Effect.Effect<void, FSErr, never> {
return Effect.async((cb) => {
const requestId = this.nextRequestId++;
performance.mark("append" + "_" + handle.filename);
this.callbacks.set(requestId, (_) => {
@@ -126,7 +131,7 @@ export class OPFSFilesystem
"append" + "_" + handle.filename,
"appendEnd" + "_" + handle.filename,
);
resolve(undefined);
cb(Effect.succeed(undefined));
});
this.opfsWorker.postMessage({
type: "append",
@@ -138,11 +143,11 @@ export class OPFSFilesystem
}
read(
handle: { id: number; filename: string },
handle: {id: number, filename: string},
offset: number,
length: number,
): Promise<Uint8Array> {
return new Promise((resolve) => {
): Effect.Effect<Uint8Array, FSErr, never> {
return Effect.async((cb) => {
const requestId = this.nextRequestId++;
performance.mark("read" + "_" + handle.filename);
this.callbacks.set(requestId, (event) => {
@@ -152,7 +157,7 @@ export class OPFSFilesystem
"read" + "_" + handle.filename,
"readEnd" + "_" + handle.filename,
);
resolve(event.data.data);
cb(Effect.succeed(event.data.data));
});
this.opfsWorker.postMessage({
type: "read",
@@ -164,8 +169,8 @@ export class OPFSFilesystem
});
}
close(handle: { id: number; filename: string }): Promise<void> {
return new Promise((resolve) => {
close(handle: {id: number, filename: string}): Effect.Effect<void, FSErr, never> {
return Effect.async((cb) => {
const requestId = this.nextRequestId++;
performance.mark("close" + "_" + handle.filename);
this.callbacks.set(requestId, (_) => {
@@ -175,7 +180,7 @@ export class OPFSFilesystem
"close" + "_" + handle.filename,
"closeEnd" + "_" + handle.filename,
);
resolve(undefined);
cb(Effect.succeed(undefined));
});
this.opfsWorker.postMessage({
type: "close",
@@ -186,20 +191,22 @@ export class OPFSFilesystem
}
closeAndRename(
handle: { id: number; filename: string },
handle: {id: number, filename: string},
filename: BlockFilename,
): Promise<void> {
return new Promise((resolve) => {
): Effect.Effect<void, FSErr, never> {
return Effect.async((cb) => {
const requestId = this.nextRequestId++;
performance.mark("closeAndRename" + "_" + handle.filename);
this.callbacks.set(requestId, () => {
performance.mark("closeAndRenameEnd" + "_" + handle.filename);
performance.mark(
"closeAndRenameEnd" + "_" + handle.filename,
);
performance.measure(
"closeAndRename" + "_" + handle.filename,
"closeAndRename" + "_" + handle.filename,
"closeAndRenameEnd" + "_" + handle.filename,
);
resolve(undefined);
cb(Effect.succeed(undefined));
});
this.opfsWorker.postMessage({
type: "closeAndRename",
@@ -210,8 +217,10 @@ export class OPFSFilesystem
});
}
removeFile(filename: BlockFilename | WalFilename): Promise<void> {
return new Promise((resolve) => {
removeFile(
filename: BlockFilename | WalFilename,
): Effect.Effect<void, FSErr, never> {
return Effect.async((cb) => {
const requestId = this.nextRequestId++;
performance.mark("removeFile" + "_" + filename);
this.callbacks.set(requestId, () => {
@@ -221,7 +230,7 @@ export class OPFSFilesystem
"removeFile" + "_" + filename,
"removeFileEnd" + "_" + filename,
);
resolve(undefined);
cb(Effect.succeed(undefined));
});
this.opfsWorker.postMessage({
type: "removeFile",

View File

@@ -14,6 +14,7 @@ import { AccountID, LSMStorage } from "cojson";
import { AuthProvider } from "./auth/auth.js";
import { OPFSFilesystem } from "./OPFSFilesystem.js";
import { IDBStorage } from "cojson-storage-indexeddb";
import { Effect, Queue } from "effect";
import { createWebSocketPeer } from "cojson-transport-ws";
export * from "./auth/auth.js";
@@ -41,11 +42,13 @@ export async function createJazzBrowserContext<Acc extends Account>({
const crypto = customCrypto || (await WasmCrypto.create());
let sessionDone: () => void;
const firstWsPeer = createWebSocketPeer({
websocket: new WebSocket(peerAddr),
id: peerAddr + "@" + new Date().toISOString(),
role: "server",
});
const firstWsPeer = await Effect.runPromise(
createWebSocketPeer({
websocket: new WebSocket(peerAddr),
id: peerAddr + "@" + new Date().toISOString(),
role: "server",
}),
);
let shouldTryToReconnect = true;
let currentReconnectionTimeout = initialReconnectionTimeout;
@@ -109,11 +112,13 @@ export async function createJazzBrowserContext<Acc extends Account>({
});
me._raw.core.node.syncManager.addPeer(
createWebSocketPeer({
websocket: new WebSocket(peerAddr),
id: peerAddr + "@" + new Date().toISOString(),
role: "server",
})
await Effect.runPromise(
createWebSocketPeer({
websocket: new WebSocket(peerAddr),
id: peerAddr + "@" + new Date().toISOString(),
role: "server",
}),
),
);
}
}
@@ -127,7 +132,11 @@ export async function createJazzBrowserContext<Acc extends Account>({
shouldTryToReconnect = false;
window.removeEventListener("online", onOnline);
console.log("Cleaning up node");
me._raw.core.node.gracefulShutdown();
for (const peer of Object.values(
me._raw.core.node.syncManager.peers,
)) {
void Effect.runPromise(Queue.shutdown(peer.outgoing));
}
sessionDone?.();
},
};

View File

@@ -1,162 +1,5 @@
# jazz-autosub
## 0.7.34-neverthrow.3
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.3
- cojson-transport-ws@0.7.34-neverthrow.3
- jazz-tools@0.7.34-neverthrow.3
## 0.7.34-neverthrow.2
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.34-neverthrow.2
## 0.7.34-neverthrow.1
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.1
- cojson-transport-ws@0.7.34-neverthrow.1
- jazz-tools@0.7.34-neverthrow.1
## 0.7.34-neverthrow.0
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.0
- cojson-transport-ws@0.7.34-neverthrow.0
- jazz-tools@0.7.34-neverthrow.0
## 0.7.33
### Patch Changes
- Updated dependencies [fdde8db]
- Updated dependencies [b297c93]
- Updated dependencies [07fe2b9]
- Updated dependencies [3bf5127]
- Updated dependencies [a8b74ff]
- Updated dependencies [db53161]
- cojson-transport-ws@0.7.33
- cojson@0.7.33
- jazz-tools@0.7.33
## 0.7.33-hotfixes.5
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.5
- cojson-transport-ws@0.7.33-hotfixes.5
- jazz-tools@0.7.33-hotfixes.5
## 0.7.33-hotfixes.4
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.4
- cojson-transport-ws@0.7.33-hotfixes.4
- jazz-tools@0.7.33-hotfixes.4
## 0.7.33-hotfixes.3
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.33-hotfixes.3
- cojson@0.7.33-hotfixes.3
- jazz-tools@0.7.33-hotfixes.3
## 0.7.33-hotfixes.2
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.33-hotfixes.2
## 0.7.33-hotfixes.1
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.33-hotfixes.1
## 0.7.33-hotfixes.0
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.0
- cojson-transport-ws@0.7.33-hotfixes.0
- jazz-tools@0.7.33-hotfixes.0
## 0.7.32
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.32
## 0.7.31
### Patch Changes
- Updated dependencies
- Updated dependencies
- cojson-transport-ws@0.7.31
- cojson@0.7.31
- jazz-tools@0.7.31
## 0.7.30
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.30
## 0.7.29
### Patch Changes
- Updated dependencies
- cojson@0.7.29
- cojson-transport-ws@0.7.29
- jazz-tools@0.7.29
## 0.7.28
### Patch Changes
- Updated dependencies
- cojson@0.7.28
- cojson-transport-ws@0.7.28
- jazz-tools@0.7.28
## 0.7.27
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.27
## 0.7.26
### Patch Changes
- Remove Effect from jazz/cojson internals
- Updated dependencies
- cojson@0.7.26
- cojson-transport-ws@0.7.26
- jazz-tools@0.7.26
## 0.7.25
### Patch Changes

View File

@@ -5,10 +5,11 @@
"types": "src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.7.34-neverthrow.3",
"version": "0.7.25",
"dependencies": {
"cojson": "workspace:*",
"cojson-transport-ws": "workspace:*",
"effect": "^3.5.2",
"jazz-tools": "workspace:*",
"ws": "^8.14.2"
},

View File

@@ -1,6 +1,7 @@
import { AgentSecret, Peer, SessionID, WasmCrypto } from "cojson";
import { createWebSocketPeer } from "cojson-transport-ws";
import { Account, CoValueClass, ID } from "jazz-tools";
import { Effect } from "effect";
import { WebSocket } from "ws";
/** @category Context Creation */
@@ -17,11 +18,13 @@ export async function startWorker<Acc extends Account>({
syncServer?: string;
accountSchema?: CoValueClass<Acc> & typeof Account;
}): Promise<{ worker: Acc }> {
const wsPeer: Peer = createWebSocketPeer({
id: "upstream",
websocket: new WebSocket(peer),
role: "server",
});
const wsPeer: Peer = await Effect.runPromise(
createWebSocketPeer({
id: "upstream",
websocket: new WebSocket(peer),
role: "server",
}),
);
if (!accountID) {
throw new Error("No accountID provided");
@@ -49,11 +52,13 @@ export async function startWorker<Acc extends Account>({
if (!worker._raw.core.node.syncManager.peers["upstream"]) {
console.log(new Date(), "Reconnecting to upstream " + peer);
const wsPeer: Peer = createWebSocketPeer({
id: "upstream",
websocket: new WebSocket(peer),
role: "server",
});
const wsPeer: Peer = await Effect.runPromise(
createWebSocketPeer({
id: "upstream",
websocket: new WebSocket(peer),
role: "server",
}),
);
worker._raw.core.node.syncManager.addPeer(wsPeer);
}

View File

@@ -1,155 +1,5 @@
# jazz-react
## 0.7.34-neverthrow.3
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.3
- jazz-browser@0.7.34-neverthrow.3
- jazz-tools@0.7.34-neverthrow.3
## 0.7.34-neverthrow.2
### Patch Changes
- jazz-browser@0.7.34-neverthrow.2
## 0.7.34-neverthrow.1
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.1
- jazz-browser@0.7.34-neverthrow.1
- jazz-tools@0.7.34-neverthrow.1
## 0.7.34-neverthrow.0
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.0
- jazz-browser@0.7.34-neverthrow.0
- jazz-tools@0.7.34-neverthrow.0
## 0.7.33
### Patch Changes
- Updated dependencies [b297c93]
- Updated dependencies [3bf5127]
- Updated dependencies [a8b74ff]
- Updated dependencies [db53161]
- cojson@0.7.33
- jazz-browser@0.7.33
- jazz-tools@0.7.33
## 0.7.33-hotfixes.5
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.5
- jazz-browser@0.7.33-hotfixes.5
- jazz-tools@0.7.33-hotfixes.5
## 0.7.33-hotfixes.4
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.4
- jazz-browser@0.7.33-hotfixes.4
- jazz-tools@0.7.33-hotfixes.4
## 0.7.33-hotfixes.3
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.3
- jazz-browser@0.7.33-hotfixes.3
- jazz-tools@0.7.33-hotfixes.3
## 0.7.33-hotfixes.2
### Patch Changes
- jazz-browser@0.7.33-hotfixes.2
## 0.7.33-hotfixes.1
### Patch Changes
- jazz-browser@0.7.33-hotfixes.1
## 0.7.33-hotfixes.0
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.0
- jazz-browser@0.7.33-hotfixes.0
- jazz-tools@0.7.33-hotfixes.0
## 0.7.32
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.32
- jazz-browser@0.7.32
## 0.7.31
### Patch Changes
- Updated dependencies
- cojson@0.7.31
- jazz-browser@0.7.31
- jazz-tools@0.7.31
## 0.7.30
### Patch Changes
- jazz-browser@0.7.30
## 0.7.29
### Patch Changes
- Updated dependencies
- cojson@0.7.29
- jazz-browser@0.7.29
- jazz-tools@0.7.29
## 0.7.28
### Patch Changes
- Updated dependencies
- cojson@0.7.28
- jazz-browser@0.7.28
- jazz-tools@0.7.28
## 0.7.27
### Patch Changes
- jazz-browser@0.7.27
## 0.7.26
### Patch Changes
- Remove Effect from jazz/cojson internals
- Updated dependencies
- cojson@0.7.26
- jazz-browser@0.7.26
- jazz-tools@0.7.26
## 0.7.25
### Patch Changes

View File

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

View File

@@ -1,161 +1,5 @@
# jazz-autosub
## 0.7.34-neverthrow.3
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.3
- cojson-transport-ws@0.7.34-neverthrow.3
- jazz-tools@0.7.34-neverthrow.3
## 0.7.34-neverthrow.2
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.34-neverthrow.2
## 0.7.34-neverthrow.1
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.1
- cojson-transport-ws@0.7.34-neverthrow.1
- jazz-tools@0.7.34-neverthrow.1
## 0.7.34-neverthrow.0
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.0
- cojson-transport-ws@0.7.34-neverthrow.0
- jazz-tools@0.7.34-neverthrow.0
## 0.7.33
### Patch Changes
- Updated dependencies [fdde8db]
- Updated dependencies [b297c93]
- Updated dependencies [07fe2b9]
- Updated dependencies [3bf5127]
- Updated dependencies [a8b74ff]
- Updated dependencies [db53161]
- cojson-transport-ws@0.7.33
- cojson@0.7.33
- jazz-tools@0.7.33
## 0.7.33-hotfixes.5
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.5
- cojson-transport-ws@0.7.33-hotfixes.5
- jazz-tools@0.7.33-hotfixes.5
## 0.7.33-hotfixes.4
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.4
- cojson-transport-ws@0.7.33-hotfixes.4
- jazz-tools@0.7.33-hotfixes.4
## 0.7.33-hotfixes.3
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.33-hotfixes.3
- cojson@0.7.33-hotfixes.3
- jazz-tools@0.7.33-hotfixes.3
## 0.7.33-hotfixes.2
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.33-hotfixes.2
## 0.7.33-hotfixes.1
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.33-hotfixes.1
## 0.7.33-hotfixes.0
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.0
- cojson-transport-ws@0.7.33-hotfixes.0
- jazz-tools@0.7.33-hotfixes.0
## 0.7.32
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.32
## 0.7.31
### Patch Changes
- Updated dependencies
- Updated dependencies
- cojson-transport-ws@0.7.31
- cojson@0.7.31
- jazz-tools@0.7.31
## 0.7.30
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.30
## 0.7.29
### Patch Changes
- Updated dependencies
- cojson@0.7.29
- cojson-transport-ws@0.7.29
- jazz-tools@0.7.29
## 0.7.28
### Patch Changes
- Updated dependencies
- cojson@0.7.28
- cojson-transport-ws@0.7.28
- jazz-tools@0.7.28
## 0.7.27
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.27
## 0.7.26
### Patch Changes
- Updated dependencies
- cojson@0.7.26
- cojson-transport-ws@0.7.26
- jazz-tools@0.7.26
## 0.7.25
### Patch Changes

View File

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

View File

@@ -25,7 +25,7 @@ const accountCreate = Command.make(
return Effect.gen(function* () {
const crypto = yield* Effect.promise(() => WasmCrypto.create());
const peer = createWebSocketPeer({
const peer = yield* createWebSocketPeer({
id: "upstream",
websocket: new WebSocket(peerAddr),
role: "server",
@@ -53,7 +53,7 @@ const accountCreate = Command.make(
),
);
const peer2 = createWebSocketPeer({
const peer2 = yield* createWebSocketPeer({
id: "upstream2",
websocket: new WebSocket(peerAddr),
role: "server",

View File

@@ -1,99 +1,5 @@
# jazz-autosub
## 0.7.34-neverthrow.3
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.3
## 0.7.34-neverthrow.1
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.1
## 0.7.34-neverthrow.0
### Patch Changes
- Updated dependencies
- cojson@0.7.34-neverthrow.0
## 0.7.33
### Patch Changes
- Updated dependencies [b297c93]
- Updated dependencies [3bf5127]
- Updated dependencies [a8b74ff]
- Updated dependencies [db53161]
- cojson@0.7.33
## 0.7.33-hotfixes.5
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.5
## 0.7.33-hotfixes.4
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.4
## 0.7.33-hotfixes.3
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.3
## 0.7.33-hotfixes.0
### Patch Changes
- Updated dependencies
- cojson@0.7.33-hotfixes.0
## 0.7.32
### Patch Changes
- Adapt type of applyDiff to make CoMaps fully subclassable again
## 0.7.31
### Patch Changes
- Updated dependencies
- cojson@0.7.31
## 0.7.29
### Patch Changes
- Updated dependencies
- cojson@0.7.29
## 0.7.28
### Patch Changes
- Updated dependencies
- cojson@0.7.28
## 0.7.26
### Patch Changes
- Remove Effect from jazz/cojson internals
- Updated dependencies
- cojson@0.7.26
## 0.7.25
### Patch Changes

View File

@@ -5,9 +5,11 @@
"types": "./src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.7.34-neverthrow.3",
"version": "0.7.25",
"dependencies": {
"@effect/schema": "^0.66.16",
"cojson": "workspace:*",
"effect": "^3.5.2",
"fast-check": "^3.17.2"
},
"scripts": {

View File

@@ -11,6 +11,7 @@ import type {
RawControlledAccount,
SessionID,
} from "cojson";
import { Context, Effect } from "effect";
import {
CoMap,
CoValue,
@@ -220,10 +221,12 @@ export class Account extends CoValueBase implements CoValue {
},
) {
// TODO: is there a cleaner way to do this?
const connectedPeers = cojsonInternals.connectedPeers(
"creatingAccount",
"createdAccount",
{ peer1role: "server", peer2role: "client" },
const connectedPeers = await Effect.runPromise(
cojsonInternals.connectedPeers(
"creatingAccount",
"createdAccount",
{ peer1role: "server", peer2role: "client" },
),
);
as._raw.core.node.syncManager.addPeer(connectedPeers[1]);
@@ -375,6 +378,9 @@ export const AccountAndGroupProxyHandler: ProxyHandler<Account | Group> = {
},
};
/** @category Identity & Permissions */
export class AccountCtx extends Context.Tag("Account")<AccountCtx, Account>() {}
/** @category Identity & Permissions */
export function isControlledAccount(account: Account): account is Account & {
isMe: true;

View File

@@ -1,4 +1,4 @@
import type { JsonValue, RawCoList } from "cojson";
import type { RawCoList } from "cojson";
import { RawAccount } from "cojson";
import type {
CoValue,
@@ -26,8 +26,8 @@ import {
makeRefs,
subscribeToCoValue,
subscribeToExistingCoValue,
subscriptionsScopes,
} from "../internal.js";
import { encodeSync, decodeSync } from "@effect/schema/Schema";
/**
* CoLists are collaborative versions of plain arrays.
@@ -303,7 +303,7 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
} else if ("encoded" in itemDescriptor) {
return this._raw
.asArray()
.map((e) => itemDescriptor.encoded.encode(e));
.map((e) => encodeSync(itemDescriptor.encoded)(e));
} else if (isRefEncoded(itemDescriptor)) {
return this.map((item, idx) =>
seenAbove?.includes((item as CoValue)?.id)
@@ -444,21 +444,16 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
castAs<Cl extends CoValueClass & CoValueFromRaw<CoValue>>(
cl: Cl,
): InstanceType<Cl> {
const casted = cl.fromRaw(this._raw) as InstanceType<Cl>;
const subscriptionScope = subscriptionsScopes.get(this);
if (subscriptionScope) {
subscriptionsScopes.set(casted, subscriptionScope);
}
return casted;
return cl.fromRaw(this._raw) as InstanceType<Cl>;
}
}
function toRawItems<Item>(items: Item[], itemDescriptor: Schema) {
const rawItems =
itemDescriptor === "json"
? (items as JsonValue[])
? items
: "encoded" in itemDescriptor
? items?.map((e) => itemDescriptor.encoded.encode(e))
? items?.map((e) => encodeSync(itemDescriptor.encoded)(e))
: isRefEncoded(itemDescriptor)
? items?.map((v) => (v as unknown as CoValue).id)
: (() => {
@@ -477,7 +472,7 @@ const CoListProxyHandler: ProxyHandler<CoList> = {
} else if ("encoded" in itemDescriptor) {
return rawValue === undefined
? undefined
: itemDescriptor.encoded.decode(rawValue);
: decodeSync(itemDescriptor.encoded)(rawValue);
} else if (isRefEncoded(itemDescriptor)) {
return rawValue === undefined
? undefined
@@ -510,7 +505,7 @@ const CoListProxyHandler: ProxyHandler<CoList> = {
if (itemDescriptor === "json") {
rawValue = value;
} else if ("encoded" in itemDescriptor) {
rawValue = itemDescriptor.encoded.encode(value);
rawValue = encodeSync(itemDescriptor.encoded)(value);
} else if (isRefEncoded(itemDescriptor)) {
rawValue = value.id;
}

View File

@@ -1,4 +1,6 @@
import type { JsonValue, RawCoMap } from "cojson";
import type { Simplify } from "effect/Types";
import { encodeSync, decodeSync } from "@effect/schema/Schema";
import type {
CoValue,
Schema,
@@ -34,10 +36,6 @@ type CoMapEdit<V> = {
madeAt: Date;
};
export type Simplify<A> = {
[K in keyof A]: A[K]
} extends infer B ? B : never
/**
* CoMaps are collaborative versions of plain objects, mapping string-like keys to values.
*
@@ -149,7 +147,7 @@ export class CoMap extends CoValueBase implements CoValue {
descriptor === "json"
? rawEdit.value
: "encoded" in descriptor
? descriptor.encoded.encode(rawEdit.value)
? decodeSync(descriptor.encoded)(rawEdit.value)
: new Ref(
rawEdit.value as ID<CoValue>,
target._loadedAs,
@@ -319,7 +317,7 @@ export class CoMap extends CoValueBase implements CoValue {
rawInit[key] = (initValue as unknown as CoValue).id;
}
} else if ("encoded" in descriptor) {
rawInit[key] = descriptor.encoded.encode(
rawInit[key] = encodeSync(descriptor.encoded)(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initValue as any,
);
@@ -457,25 +455,25 @@ export class CoMap extends CoValueBase implements CoValue {
return subscribeToExistingCoValue(this, depth, listener);
}
applyDiff<N extends Partial<CoMapInit<this>>>(newValues: N) {
applyDiff(newValues: Partial<CoMapInit<this>>) {
for (const key in newValues) {
if (Object.prototype.hasOwnProperty.call(newValues, key)) {
const tKey = key as keyof typeof newValues & keyof this;
const descriptor = (this._schema[tKey as string] ||
this._schema[ItemsSym]) as Schema;
if (tKey in this._schema) {
const newValue = newValues[tKey];
const currentValue = (this as unknown as N)[tKey];
const currentValue = this[tKey];
if (descriptor === "json" || "encoded" in descriptor) {
if (currentValue !== newValue) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this as any)[tKey] = newValue;
}
} else if (isRefEncoded(descriptor)) {
const currentId = (currentValue as CoValue | undefined)
?.id;
}
else if (isRefEncoded(descriptor)) {
const currentId = (currentValue as CoValue | undefined)?.id;
const newId = (newValue as CoValue | undefined)?.id;
if (currentId !== newId) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -518,7 +516,7 @@ const CoMapProxyHandler: ProxyHandler<CoMap> = {
} else if ("encoded" in descriptor) {
return raw === undefined
? undefined
: descriptor.encoded.decode(raw);
: decodeSync(descriptor.encoded)(raw);
} else if (isRefEncoded(descriptor)) {
return raw === undefined
? undefined
@@ -552,7 +550,7 @@ const CoMapProxyHandler: ProxyHandler<CoMap> = {
if (descriptor === "json") {
target._raw.set(key, value);
} else if ("encoded" in descriptor) {
target._raw.set(key, descriptor.encoded.encode(value));
target._raw.set(key, encodeSync(descriptor.encoded)(value));
} else if (isRefEncoded(descriptor)) {
target._raw.set(key, value.id);
subscriptionsScopes

View File

@@ -35,6 +35,7 @@ import {
ensureCoValueLoaded,
subscribeToExistingCoValue,
} from "../internal.js";
import { encodeSync, decodeSync } from "@effect/schema/Schema";
export type CoStreamEntry<Item> = SingleCoStreamEntry<Item> & {
all: IterableIterator<SingleCoStreamEntry<Item>>;
@@ -140,7 +141,7 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
if (itemDescriptor === "json") {
this._raw.push(item as JsonValue);
} else if ("encoded" in itemDescriptor) {
this._raw.push(itemDescriptor.encoded.encode(item));
this._raw.push(encodeSync(itemDescriptor.encoded)(item));
} else if (isRefEncoded(itemDescriptor)) {
this._raw.push((item as unknown as CoValue).id);
}
@@ -152,7 +153,7 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
itemDescriptor === "json"
? (v: unknown) => v
: "encoded" in itemDescriptor
? itemDescriptor.encoded.encode
? encodeSync(itemDescriptor.encoded)
: (v: unknown) => v && (v as CoValue).id;
return {
@@ -246,7 +247,7 @@ function entryFromRawEntry<Item>(
? (CoValue & Item) | null
: Item;
} else if ("encoded" in itemField) {
return itemField.encoded.decode(rawEntry.value);
return decodeSync(itemField.encoded)(rawEntry.value);
} else if (isRefEncoded(itemField)) {
return this.ref?.accessFrom(
accessFrom,

View File

@@ -122,12 +122,7 @@ export class CoValueBase implements CoValue {
castAs<Cl extends CoValueClass & CoValueFromRaw<CoValue>>(
cl: Cl,
): InstanceType<Cl> {
const casted = cl.fromRaw(this._raw) as InstanceType<Cl>;
const subscriptionScope = subscriptionsScopes.get(this);
if (subscriptionScope) {
subscriptionsScopes.set(casted, subscriptionScope);
}
return casted;
return cl.fromRaw(this._raw) as InstanceType<Cl>;
}
}

View File

@@ -5,6 +5,7 @@ import {
isCoValueClass,
CoValueFromRaw,
} from "../internal.js";
import type { Schema as EffectSchema, TypeId } from "@effect/schema/Schema";
export type CoMarker = { readonly __co: unique symbol };
/** @category Schema definition */
@@ -112,7 +113,7 @@ function ref<
}
export type JsonEncoded = "json";
export type EncodedAs<V> = { encoded: Encoder<V> | OptionalEncoder<V> };
export type EncodedAs<V> = { encoded: Encoder<V> };
export type RefEncoded<V extends CoValue> = {
ref: CoValueClass<V> | ((raw: RawCoValue) => CoValueClass<V>);
optional: boolean;
@@ -151,23 +152,31 @@ export type SchemaFor<Field> = NonNullable<Field> extends CoValue
? JsonEncoded
: EncodedAs<NonNullable<Field>>;
export type Encoder<V> = {
encode: (value: V) => JsonValue;
decode: (value: JsonValue) => V;
export type EffectSchemaWithInputAndOutput<A, I = A> = EffectSchema<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any,
never
> & {
[TypeId]: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_A: (_: any) => A;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_I: (_: any) => I;
};
};
export type OptionalEncoder<V> =
| Encoder<V>
| {
encode: (value: V | undefined) => JsonValue;
decode: (value: JsonValue) => V | undefined;
};
export type Encoder<V> = EffectSchemaWithInputAndOutput<V, JsonValue>;
export type OptionalEncoder<V> = EffectSchemaWithInputAndOutput<
V,
JsonValue | undefined
>;
import { Date } from "@effect/schema/Schema";
import { SchemaInit, ItemsSym, MembersSym } from "./symbols.js";
/** @category Schema definition */
export const Encoders = {
Date: {
encode: (value: Date) => value.toISOString(),
decode: (value: JsonValue) => new Date(value as string),
},
Date,
};

View File

@@ -1,12 +1,12 @@
import { expect, describe, test } from "vitest";
import { connectedPeers } from "cojson/src/streamUtils.js";
import { newRandomSessionID } from "cojson/src/coValueCore.js";
import { Effect, Queue } from "effect";
import {
Account,
CoList,
WasmCrypto,
co,
cojsonInternals,
isControlledAccount,
} from "../index.js";
@@ -157,13 +157,11 @@ describe("CoList resolution", async () => {
test("Loading and availability", async () => {
const { me, list } = await initNodeAndList();
const [initialAsPeer, secondPeer] = connectedPeers(
"initial",
"second",
{
const [initialAsPeer, secondPeer] = await Effect.runPromise(
connectedPeers("initial", "second", {
peer1role: "server",
peer2role: "client",
},
}),
);
if (!isControlledAccount(me)) {
throw "me is not a controlled account";
@@ -219,13 +217,11 @@ describe("CoList resolution", async () => {
test("Subscription & auto-resolution", async () => {
const { me, list } = await initNodeAndList();
const [initialAsPeer, secondPeer] = connectedPeers(
"initial",
"second",
{
const [initialAsPeer, secondPeer] = await Effect.runPromise(
connectedPeers("initial", "second", {
peer1role: "server",
peer2role: "client",
},
}),
);
if (!isControlledAccount(me)) {
throw "me is not a controlled account";
@@ -240,52 +236,63 @@ describe("CoList resolution", async () => {
crypto: Crypto,
});
const queue = new cojsonInternals.Channel();
await Effect.runPromise(
Effect.gen(function* ($) {
const queue = yield* $(Queue.unbounded<TestList>());
TestList.subscribe(list.id, meOnSecondPeer, [], (subscribedList) => {
console.log(
"subscribedList?.[0]?.[0]?.[0]",
subscribedList?.[0]?.[0]?.[0],
);
void queue.push(subscribedList);
});
TestList.subscribe(
list.id,
meOnSecondPeer,
[],
(subscribedList) => {
console.log(
"subscribedList?.[0]?.[0]?.[0]",
subscribedList?.[0]?.[0]?.[0],
);
void Effect.runPromise(
Queue.offer(queue, subscribedList),
);
},
);
const update1 = (await queue.next()).value;
expect(update1?.[0]).toBe(null);
const update1 = yield* $(Queue.take(queue));
expect(update1?.[0]).toBe(null);
const update2 = (await queue.next()).value;
expect(update2?.[0]).toBeDefined();
expect(update2?.[0]?.[0]).toBe(null);
const update2 = yield* $(Queue.take(queue));
expect(update2?.[0]).toBeDefined();
expect(update2?.[0]?.[0]).toBe(null);
const update3 = (await queue.next()).value;
expect(update3?.[0]?.[0]).toBeDefined();
expect(update3?.[0]?.[0]?.[0]).toBe("a");
expect(update3?.[0]?.[0]?.joined()).toBe("a,b");
const update3 = yield* $(Queue.take(queue));
expect(update3?.[0]?.[0]).toBeDefined();
expect(update3?.[0]?.[0]?.[0]).toBe("a");
expect(update3?.[0]?.[0]?.joined()).toBe("a,b");
update3[0]![0]![0] = "x";
update3[0]![0]![0] = "x";
const update4 = (await queue.next()).value;
expect(update4?.[0]?.[0]?.[0]).toBe("x");
const update4 = yield* $(Queue.take(queue));
expect(update4?.[0]?.[0]?.[0]).toBe("x");
// When assigning a new nested value, we get an update
// When assigning a new nested value, we get an update
const newTwiceNestedList = TwiceNestedList.create(["y", "z"], {
owner: meOnSecondPeer,
});
const newTwiceNestedList = TwiceNestedList.create(["y", "z"], {
owner: meOnSecondPeer,
});
const newNestedList = NestedList.create([newTwiceNestedList], {
owner: meOnSecondPeer,
});
const newNestedList = NestedList.create([newTwiceNestedList], {
owner: meOnSecondPeer,
});
update4[0] = newNestedList;
update4[0] = newNestedList;
const update5 = (await queue.next()).value;
expect(update5?.[0]?.[0]?.[0]).toBe("y");
expect(update5?.[0]?.[0]?.joined()).toBe("y,z");
const update5 = yield* $(Queue.take(queue));
expect(update5?.[0]?.[0]?.[0]).toBe("y");
expect(update5?.[0]?.[0]?.joined()).toBe("y,z");
// we get updates when the new nested value changes
newTwiceNestedList[0] = "w";
const update6 = (await queue.next()).value;
expect(update6?.[0]?.[0]?.[0]).toBe("w");
// we get updates when the new nested value changes
newTwiceNestedList[0] = "w";
const update6 = yield* $(Queue.take(queue));
expect(update6?.[0]?.[0]?.[0]).toBe("w");
}),
);
});
});

View File

@@ -1,6 +1,7 @@
import { expect, describe, test } from "vitest";
import { connectedPeers } from "cojson/src/streamUtils.js";
import { newRandomSessionID } from "cojson/src/coValueCore.js";
import { Effect, Queue } from "effect";
import {
Account,
Encoders,
@@ -8,8 +9,8 @@ import {
co,
WasmCrypto,
isControlledAccount,
cojsonInternals,
} from "../index.js";
import { Schema } from "@effect/schema";
const Crypto = await WasmCrypto.create();
@@ -24,10 +25,7 @@ describe("Simple CoMap operations", async () => {
_height = co.number;
birthday = co.encoded(Encoders.Date);
name? = co.string;
nullable = co.optional.encoded<string | undefined>({
encode: (value: string | undefined) => value || null,
decode: (value: unknown) => (value as string) || undefined,
});
nullable = co.optional.encoded(Schema.NullishOr(Schema.String));
optionalDate = co.optional.encoded(Encoders.Date);
get roughColor() {
@@ -44,7 +42,7 @@ describe("Simple CoMap operations", async () => {
color: "red",
_height: 10,
birthday: birthday,
nullable: undefined,
nullable: null,
},
{ owner: me },
);
@@ -96,7 +94,7 @@ describe("Simple CoMap operations", async () => {
expect(map._raw.get("_height")).toEqual(20);
map.nullable = "not null";
map.nullable = undefined;
map.nullable = null;
delete map.nullable;
map.nullable = undefined;
@@ -284,12 +282,12 @@ describe("CoMap resolution", async () => {
test("Loading and availability", async () => {
const { me, map } = await initNodeAndMap();
const [initialAsPeer, secondPeer] =
const [initialAsPeer, secondPeer] = await Effect.runPromise(
connectedPeers("initial", "second", {
peer1role: "server",
peer2role: "client",
});
}),
);
if (!isControlledAccount(me)) {
throw "me is not a controlled account";
}
@@ -355,12 +353,12 @@ describe("CoMap resolution", async () => {
test("Subscription & auto-resolution", async () => {
const { me, map } = await initNodeAndMap();
const [initialAsPeer, secondAsPeer] =
const [initialAsPeer, secondAsPeer] = await Effect.runPromise(
connectedPeers("initial", "second", {
peer1role: "server",
peer2role: "client",
});
}),
);
if (!isControlledAccount(me)) {
throw "me is not a controlled account";
}
@@ -374,8 +372,9 @@ describe("CoMap resolution", async () => {
crypto: Crypto,
});
const queue = new cojsonInternals.Channel<TestMap>();
await Effect.runPromise(
Effect.gen(function* ($) {
const queue = yield* $(Queue.unbounded<TestMap>());
TestMap.subscribe(
map.id,
@@ -386,20 +385,22 @@ describe("CoMap resolution", async () => {
"subscribedMap.nested?.twiceNested?.taste",
subscribedMap.nested?.twiceNested?.taste,
);
void queue.push(subscribedMap);
void Effect.runPromise(
Queue.offer(queue, subscribedMap),
);
},
);
const update1 = (await queue.next()).value;
const update1 = yield* $(Queue.take(queue));
expect(update1.nested).toEqual(null);
const update2 = (await queue.next()).value;
const update2 = yield* $(Queue.take(queue));
expect(update2.nested?.name).toEqual("nested");
map.nested!.name = "nestedUpdated";
const _ = (await queue.next()).value;
const update3 = (await queue.next()).value;
const _ = yield* $(Queue.take(queue));
const update3 = yield* $(Queue.take(queue));
expect(update3.nested?.name).toEqual("nestedUpdated");
const oldTwiceNested = update3.nested!.twiceNested;
@@ -423,21 +424,23 @@ describe("CoMap resolution", async () => {
update3.nested = newNested;
(await queue.next()).value;
// const update4 = (await queue.next()).value;
const update4b = (await queue.next()).value;
yield* $(Queue.take(queue));
// const update4 = yield* $(Queue.take(queue));
const update4b = yield* $(Queue.take(queue));
expect(update4b.nested?.name).toEqual("newNested");
expect(update4b.nested?.twiceNested?.taste).toEqual("sweet");
// we get updates when the new nested value changes
newTwiceNested.taste = "salty";
const update5 = (await queue.next()).value;
const update5 = yield* $(Queue.take(queue));
expect(update5.nested?.twiceNested?.taste).toEqual("salty");
newTwiceNested.taste = "umami";
const update6 = (await queue.next()).value;
const update6 = yield* $(Queue.take(queue));
expect(update6.nested?.twiceNested?.taste).toEqual("umami");
}),
);
});
class TestMapWithOptionalRef extends CoMap {

View File

@@ -1,6 +1,7 @@
import { expect, describe, test } from "vitest";
import { connectedPeers } from "cojson/src/streamUtils.js";
import { newRandomSessionID } from "cojson/src/coValueCore.js";
import { Effect, Queue } from "effect";
import {
BinaryCoStream,
ID,
@@ -9,7 +10,6 @@ import {
co,
WasmCrypto,
isControlledAccount,
cojsonInternals,
} from "../index.js";
const Crypto = await WasmCrypto.create();
@@ -83,13 +83,11 @@ describe("CoStream resolution", async () => {
test("Loading and availability", async () => {
const { me, stream } = await initNodeAndStream();
const [initialAsPeer, secondPeer] = connectedPeers(
"initial",
"second",
{
const [initialAsPeer, secondPeer] = await Effect.runPromise(
connectedPeers("initial", "second", {
peer1role: "server",
peer2role: "client",
},
}),
);
if (!isControlledAccount(me)) {
throw "me is not a controlled account";
@@ -178,15 +176,12 @@ describe("CoStream resolution", async () => {
test("Subscription & auto-resolution", async () => {
const { me, stream } = await initNodeAndStream();
const [initialAsPeer, secondAsPeer] = connectedPeers(
"initial",
"second",
{
const [initialAsPeer, secondAsPeer] = await Effect.runPromise(
connectedPeers("initial", "second", {
peer1role: "server",
peer2role: "client",
},
}),
);
me._raw.core.node.syncManager.addPeer(secondAsPeer);
if (!isControlledAccount(me)) {
throw "me is not a controlled account";
@@ -200,68 +195,78 @@ describe("CoStream resolution", async () => {
crypto: Crypto,
});
const queue = new cojsonInternals.Channel();
await Effect.runPromise(
Effect.gen(function* ($) {
const queue = yield* $(Queue.unbounded<TestStream>());
TestStream.subscribe(
stream.id,
meOnSecondPeer,
[],
(subscribedStream) => {
console.log("subscribedStream[me.id]", subscribedStream[me.id]);
console.log(
"subscribedStream[me.id]?.value?.[me.id]?.value",
subscribedStream[me.id]?.value?.[me.id]?.value,
TestStream.subscribe(
stream.id,
meOnSecondPeer,
[],
(subscribedStream) => {
console.log(
"subscribedStream[me.id]",
subscribedStream[me.id],
);
console.log(
"subscribedStream[me.id]?.value?.[me.id]?.value",
subscribedStream[me.id]?.value?.[me.id]?.value,
);
console.log(
"subscribedStream[me.id]?.value?.[me.id]?.value?.[me.id]?.value",
subscribedStream[me.id]?.value?.[me.id]?.value?.[
me.id
]?.value,
);
void Effect.runPromise(
Queue.offer(queue, subscribedStream),
);
},
);
console.log(
"subscribedStream[me.id]?.value?.[me.id]?.value?.[me.id]?.value",
subscribedStream[me.id]?.value?.[me.id]?.value?.[me.id]
?.value,
);
void queue.push(subscribedStream);
},
);
const update1 = (await queue.next()).value;
expect(update1[me.id]?.value).toEqual(null);
const update1 = yield* $(Queue.take(queue));
expect(update1[me.id]?.value).toEqual(null);
const update2 = (await queue.next()).value;
expect(update2[me.id]?.value).toBeDefined();
expect(update2[me.id]?.value?.[me.id]?.value).toBe(null);
const update2 = yield* $(Queue.take(queue));
expect(update2[me.id]?.value).toBeDefined();
expect(update2[me.id]?.value?.[me.id]?.value).toBe(null);
const update3 = (await queue.next()).value;
expect(update3[me.id]?.value?.[me.id]?.value).toBeDefined();
expect(update3[me.id]?.value?.[me.id]?.value?.[me.id]?.value).toBe(
"milk",
);
const update3 = yield* $(Queue.take(queue));
expect(update3[me.id]?.value?.[me.id]?.value).toBeDefined();
expect(
update3[me.id]?.value?.[me.id]?.value?.[me.id]?.value,
).toBe("milk");
update3[me.id]!.value![me.id]!.value!.push("bread");
update3[me.id]!.value![me.id]!.value!.push("bread");
const update4 = (await queue.next()).value;
expect(update4[me.id]?.value?.[me.id]?.value?.[me.id]?.value).toBe(
"bread",
);
const update4 = yield* $(Queue.take(queue));
expect(
update4[me.id]?.value?.[me.id]?.value?.[me.id]?.value,
).toBe("bread");
// When assigning a new nested stream, we get an update
const newTwiceNested = TwiceNestedStream.create(["butter"], {
owner: meOnSecondPeer,
});
// When assigning a new nested stream, we get an update
const newTwiceNested = TwiceNestedStream.create(["butter"], {
owner: meOnSecondPeer,
});
const newNested = NestedStream.create([newTwiceNested], {
owner: meOnSecondPeer,
});
const newNested = NestedStream.create([newTwiceNested], {
owner: meOnSecondPeer,
});
update4.push(newNested);
update4.push(newNested);
const update5 = (await queue.next()).value;
expect(update5[me.id]?.value?.[me.id]?.value?.[me.id]?.value).toBe(
"butter",
);
const update5 = yield* $(Queue.take(queue));
expect(
update5[me.id]?.value?.[me.id]?.value?.[me.id]?.value,
).toBe("butter");
// we get updates when the new nested stream changes
newTwiceNested.push("jam");
const update6 = (await queue.next()).value;
expect(update6[me.id]?.value?.[me.id]?.value?.[me.id]?.value).toBe(
"jam",
// we get updates when the new nested stream changes
newTwiceNested.push("jam");
const update6 = yield* $(Queue.take(queue));
expect(
update6[me.id]?.value?.[me.id]?.value?.[me.id]?.value,
).toBe("jam");
}),
);
});
});
@@ -322,13 +327,11 @@ describe("BinaryCoStream loading & Subscription", async () => {
test("Loading and availability", async () => {
const { me, stream } = await initNodeAndStream();
const [initialAsPeer, secondAsPeer] = connectedPeers(
"initial",
"second",
{
const [initialAsPeer, secondAsPeer] = await Effect.runPromise(
connectedPeers("initial", "second", {
peer1role: "server",
peer2role: "client",
},
}),
);
if (!isControlledAccount(me)) {
throw "me is not a controlled account";
@@ -357,83 +360,98 @@ describe("BinaryCoStream loading & Subscription", async () => {
});
test("Subscription", async () => {
const { me } = await initNodeAndStream();
const stream = BinaryCoStream.create({ owner: me });
await Effect.runPromise(
Effect.gen(function* ($) {
const { me } = yield* Effect.promise(() => initNodeAndStream());
const [initialAsPeer, secondAsPeer] = connectedPeers(
"initial",
"second",
{ peer1role: "server", peer2role: "client" },
const stream = BinaryCoStream.create({ owner: me });
const [initialAsPeer, secondAsPeer] = yield* connectedPeers(
"initial",
"second",
{ peer1role: "server", peer2role: "client" },
);
me._raw.core.node.syncManager.addPeer(secondAsPeer);
if (!isControlledAccount(me)) {
throw "me is not a controlled account";
}
const meOnSecondPeer = yield* Effect.promise(() =>
Account.become({
accountID: me.id,
accountSecret: me._raw.agentSecret,
peersToLoadFrom: [initialAsPeer],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sessionID: newRandomSessionID(me.id as any),
crypto: Crypto,
}),
);
const queue = yield* $(Queue.unbounded<BinaryCoStream>());
BinaryCoStream.subscribe(
stream.id,
meOnSecondPeer,
[],
(subscribedStream) => {
void Effect.runPromise(
Queue.offer(queue, subscribedStream),
);
},
);
const update1 = yield* $(Queue.take(queue));
expect(update1.getChunks()).toBe(undefined);
stream.start({ mimeType: "text/plain" });
const update2 = yield* $(Queue.take(queue));
expect(update2.getChunks({ allowUnfinished: true })).toEqual({
mimeType: "text/plain",
fileName: undefined,
chunks: [],
totalSizeBytes: undefined,
finished: false,
});
stream.push(new Uint8Array([1, 2, 3]));
const update3 = yield* $(Queue.take(queue));
expect(update3.getChunks({ allowUnfinished: true })).toEqual({
mimeType: "text/plain",
fileName: undefined,
chunks: [new Uint8Array([1, 2, 3])],
totalSizeBytes: undefined,
finished: false,
});
stream.push(new Uint8Array([4, 5, 6]));
const update4 = yield* $(Queue.take(queue));
expect(update4.getChunks({ allowUnfinished: true })).toEqual({
mimeType: "text/plain",
fileName: undefined,
chunks: [
new Uint8Array([1, 2, 3]),
new Uint8Array([4, 5, 6]),
],
totalSizeBytes: undefined,
finished: false,
});
stream.end();
const update5 = yield* $(Queue.take(queue));
expect(update5.getChunks()).toEqual({
mimeType: "text/plain",
fileName: undefined,
chunks: [
new Uint8Array([1, 2, 3]),
new Uint8Array([4, 5, 6]),
],
totalSizeBytes: undefined,
finished: true,
});
}),
);
me._raw.core.node.syncManager.addPeer(secondAsPeer);
if (!isControlledAccount(me)) {
throw "me is not a controlled account";
}
const meOnSecondPeer = await Account.become({
accountID: me.id,
accountSecret: me._raw.agentSecret,
peersToLoadFrom: [initialAsPeer],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sessionID: newRandomSessionID(me.id as any),
crypto: Crypto,
});
const queue = new cojsonInternals.Channel();
BinaryCoStream.subscribe(
stream.id,
meOnSecondPeer,
[],
(subscribedStream) => {
void queue.push(subscribedStream);
},
);
const update1 = (await queue.next()).value;
expect(update1.getChunks()).toBe(undefined);
stream.start({ mimeType: "text/plain" });
const update2 = (await queue.next()).value;
expect(update2.getChunks({ allowUnfinished: true })).toEqual({
mimeType: "text/plain",
fileName: undefined,
chunks: [],
totalSizeBytes: undefined,
finished: false,
});
stream.push(new Uint8Array([1, 2, 3]));
const update3 = (await queue.next()).value;
expect(update3.getChunks({ allowUnfinished: true })).toEqual({
mimeType: "text/plain",
fileName: undefined,
chunks: [new Uint8Array([1, 2, 3])],
totalSizeBytes: undefined,
finished: false,
});
stream.push(new Uint8Array([4, 5, 6]));
const update4 = (await queue.next()).value;
expect(update4.getChunks({ allowUnfinished: true })).toEqual({
mimeType: "text/plain",
fileName: undefined,
chunks: [new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6])],
totalSizeBytes: undefined,
finished: false,
});
stream.end();
const update5 = (await queue.next()).value;
expect(update5.getChunks()).toEqual({
mimeType: "text/plain",
fileName: undefined,
chunks: [new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6])],
totalSizeBytes: undefined,
finished: true,
});
});
});

View File

@@ -14,6 +14,7 @@ import {
ID,
} from "../index.js";
import { newRandomSessionID } from "cojson/src/coValueCore.js";
import { Effect } from "effect";
class TestMap extends CoMap {
list = co.ref(TestList);
@@ -38,11 +39,12 @@ describe("Deep loading with depth arg", async () => {
crypto: Crypto,
});
const [initialAsPeer, secondPeer] = connectedPeers("initial", "second", {
peer1role: "server",
peer2role: "client",
});
const [initialAsPeer, secondPeer] = await Effect.runPromise(
connectedPeers("initial", "second", {
peer1role: "server",
peer2role: "client",
}),
);
if (!isControlledAccount(me)) {
throw "me is not a controlled account";
}
@@ -138,8 +140,8 @@ describe("Deep loading with depth arg", async () => {
throw new Error("map4 is undefined");
}
expect(map4.list[0]?.stream).not.toBe(null);
expect(map4.list[0]?.stream?.[me.id]).toBe(undefined)
expect(map4.list[0]?.stream?.byMe?.value).toBe(undefined);
expect(map4.list[0]?.stream?.[me.id]?.value).toBe(null);
expect(map4.list[0]?.stream?.byMe?.value).toBe(null);
const map5 = await TestMap.load(map.id, meOnSecondPeer, {
list: [{ stream: [{}] }],
@@ -252,15 +254,15 @@ test("Deep loading a record-like coMap", async () => {
crypto: Crypto,
});
const [initialAsPeer, secondPeer] = connectedPeers("initial", "second", {
peer1role: "server",
peer2role: "client",
});
const [initialAsPeer, secondPeer] = await Effect.runPromise(
connectedPeers("initial", "second", {
peer1role: "server",
peer2role: "client",
}),
);
if (!isControlledAccount(me)) {
throw "me is not a controlled account";
}
me._raw.core.node.syncManager.addPeer(secondPeer);
const meOnSecondPeer = await Account.become({
accountID: me.id,

134
pnpm-lock.yaml generated
View File

@@ -156,6 +156,9 @@ importers:
cojson-transport-ws:
specifier: workspace:*
version: link:../../packages/cojson-transport-ws
effect:
specifier: ^3.5.2
version: 3.5.2
hash-slash:
specifier: workspace:*
version: link:../../packages/hash-slash
@@ -365,6 +368,9 @@ importers:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
react-hook-form:
specifier: ^7.52.2
version: 7.52.2(react@18.2.0)
react-router:
specifier: ^6.16.0
version: 6.21.0(react@18.2.0)
@@ -441,15 +447,12 @@ importers:
'@scure/base':
specifier: ^1.1.1
version: 1.1.5
effect:
specifier: ^3.5.2
version: 3.5.2
hash-wasm:
specifier: ^4.9.0
version: 4.11.0
neverthrow:
specifier: ^7.0.1
version: 7.0.1
queueable:
specifier: ^5.3.2
version: 5.3.2
devDependencies:
'@types/jest':
specifier: ^29.5.3
@@ -466,9 +469,6 @@ importers:
eslint-config-prettier:
specifier: ^9.1.0
version: 9.1.0(eslint@8.56.0)
eslint-plugin-neverthrow:
specifier: ^1.1.4
version: 1.1.4(@typescript-eslint/parser@6.15.0(eslint@8.56.0)(typescript@5.0.2))(eslint@8.56.0)(typescript@5.0.2)
eslint-plugin-require-extensions:
specifier: ^0.1.3
version: 0.1.3(eslint@8.56.0)
@@ -484,6 +484,9 @@ importers:
cojson:
specifier: workspace:*
version: link:../cojson
effect:
specifier: ^3.5.2
version: 3.5.2
typescript:
specifier: ^5.1.6
version: 5.3.3
@@ -506,6 +509,9 @@ importers:
cojson:
specifier: workspace:*
version: link:../cojson
effect:
specifier: ^3.5.2
version: 3.5.2
typescript:
specifier: ^5.1.6
version: 5.3.3
@@ -519,6 +525,9 @@ importers:
cojson:
specifier: workspace:*
version: link:../cojson
effect:
specifier: ^3.5.2
version: 3.5.2
typescript:
specifier: ^5.1.6
version: 5.3.3
@@ -553,6 +562,9 @@ importers:
cojson-transport-ws:
specifier: workspace:*
version: link:../cojson-transport-ws
effect:
specifier: ^3.5.2
version: 3.5.2
jazz-tools:
specifier: workspace:*
version: link:../jazz-tools
@@ -593,6 +605,9 @@ importers:
cojson-transport-ws:
specifier: workspace:*
version: link:../cojson-transport-ws
effect:
specifier: ^3.5.2
version: 3.5.2
jazz-tools:
specifier: workspace:*
version: link:../jazz-tools
@@ -677,9 +692,15 @@ importers:
packages/jazz-tools:
dependencies:
'@effect/schema':
specifier: ^0.66.16
version: 0.66.16(effect@3.5.2)(fast-check@3.17.2)
cojson:
specifier: workspace:*
version: link:../cojson
effect:
specifier: ^3.5.2
version: 3.5.2
fast-check:
specifier: ^3.17.2
version: 3.17.2
@@ -1689,12 +1710,6 @@ packages:
'@types/chai@4.3.11':
resolution: {integrity: sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==}
'@types/eslint-utils@3.0.5':
resolution: {integrity: sha512-dGOLJqHXpjomkPgZiC7vnVSJtFIOM1Y6L01EyUhzPuD0y0wfIGiqxiGs3buUBfzxLIQHrCvZsIMDaCZ8R5IIoA==}
'@types/eslint@9.6.0':
resolution: {integrity: sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==}
'@types/estree@1.0.5':
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
@@ -2516,13 +2531,6 @@ packages:
peerDependencies:
eslint: '>=7.0.0'
eslint-plugin-neverthrow@1.1.4:
resolution: {integrity: sha512-+8zsE5rDqsDfKYAOq0Fr2jbuxHXTmntIWWJqJA3ms1GAKcVCjl0ycetzOu/hTxot9ctr+WYQpCBgB3F2HATR7A==}
engines: {node: '>=14.17'}
peerDependencies:
'@typescript-eslint/parser': '>=4.20.0'
eslint: '>=5.16.0'
eslint-plugin-react-hooks@4.6.0:
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
engines: {node: '>=10'}
@@ -2544,16 +2552,6 @@ packages:
resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
eslint-utils@3.0.0:
resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
peerDependencies:
eslint: '>=5'
eslint-visitor-keys@2.1.0:
resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==}
engines: {node: '>=10'}
eslint-visitor-keys@3.4.3:
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -2644,9 +2642,6 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
fast-list@1.0.3:
resolution: {integrity: sha512-Lm56Ci3EqefHNdIneRFuzhpPcpVVBz9fgqVmG3UQIxAefJv1mEYsZ1WQLTWqmdqeGEwbI2t6fbZgp9TqTYARuA==}
fast-loops@1.1.3:
resolution: {integrity: sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==}
@@ -3413,10 +3408,6 @@ packages:
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
engines: {node: '>= 0.4.0'}
neverthrow@7.0.1:
resolution: {integrity: sha512-NY1sD1wZn6dwwzV9/5KZ4wRIpH4xC7ARy3fGmnEi2Pi8CHDTWlp+WSF+if8cudrwuCWGrk2y2BE/dnGh070iuA==}
engines: {node: '>=18'}
node-abi@3.62.0:
resolution: {integrity: sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==}
engines: {node: '>=10'}
@@ -3755,10 +3746,6 @@ packages:
queue-tick@1.0.1:
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
queueable@5.3.2:
resolution: {integrity: sha512-/2ZxV1PJh7J9Q/h9ewZ4fLMmDreUlbwrWsBnluvDoKW6Nw0gbWm5hN+kiWfdDMw1o/QTAFxV9wx4KpuN5IA7OA==}
engines: {node: '>=18'}
quick-lru@4.0.1:
resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==}
engines: {node: '>=8'}
@@ -3776,6 +3763,12 @@ packages:
peerDependencies:
react: ^18.2.0
react-hook-form@7.52.2:
resolution: {integrity: sha512-pqfPEbERnxxiNMPd0bzmt1tuaPcVccywFDpyk2uV5xCIBphHV5T8SVnX9/o3kplPE1zzKt77+YIoq+EMwJp56A==}
engines: {node: '>=18.0.0'}
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
@@ -4290,18 +4283,9 @@ packages:
'@swc/wasm':
optional: true
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
tsutils@3.21.0:
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'}
peerDependencies:
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
tty-table@4.2.3:
resolution: {integrity: sha512-Fs15mu0vGzCrj8fmJNP7Ynxt5J7praPXqFN0leZeZBXJwkMxv9cb2D454k1ltrtUSJbZ4yH4e0CynsHLxmUfFA==}
engines: {node: '>=8.0.0'}
@@ -5686,16 +5670,6 @@ snapshots:
'@types/chai@4.3.11': {}
'@types/eslint-utils@3.0.5':
dependencies:
'@types/eslint': 9.6.0
'@types/estree': 1.0.5
'@types/eslint@9.6.0':
dependencies:
'@types/estree': 1.0.5
'@types/json-schema': 7.0.15
'@types/estree@1.0.5': {}
'@types/http-cache-semantics@4.0.4': {}
@@ -6776,16 +6750,6 @@ snapshots:
dependencies:
eslint: 8.56.0
eslint-plugin-neverthrow@1.1.4(@typescript-eslint/parser@6.15.0(eslint@8.56.0)(typescript@5.0.2))(eslint@8.56.0)(typescript@5.0.2):
dependencies:
'@types/eslint-utils': 3.0.5
'@typescript-eslint/parser': 6.15.0(eslint@8.56.0)(typescript@5.0.2)
eslint: 8.56.0
eslint-utils: 3.0.0(eslint@8.56.0)
tsutils: 3.21.0(typescript@5.0.2)
transitivePeerDependencies:
- typescript
eslint-plugin-react-hooks@4.6.0(eslint@8.56.0):
dependencies:
eslint: 8.56.0
@@ -6803,13 +6767,6 @@ snapshots:
esrecurse: 4.3.0
estraverse: 5.3.0
eslint-utils@3.0.0(eslint@8.56.0):
dependencies:
eslint: 8.56.0
eslint-visitor-keys: 2.1.0
eslint-visitor-keys@2.1.0: {}
eslint-visitor-keys@3.4.3: {}
eslint@8.56.0:
@@ -6945,8 +6902,6 @@ snapshots:
fast-levenshtein@2.0.6: {}
fast-list@1.0.3: {}
fast-loops@1.1.3: {}
fast-querystring@1.1.2:
@@ -7710,8 +7665,6 @@ snapshots:
netmask@2.0.2: {}
neverthrow@7.0.1: {}
node-abi@3.62.0:
dependencies:
semver: 7.5.4
@@ -8066,10 +8019,6 @@ snapshots:
queue-tick@1.0.1: {}
queueable@5.3.2:
dependencies:
fast-list: 1.0.3
quick-lru@4.0.1: {}
quick-lru@5.1.1: {}
@@ -8087,6 +8036,10 @@ snapshots:
react: 18.2.0
scheduler: 0.23.0
react-hook-form@7.52.2(react@18.2.0):
dependencies:
react: 18.2.0
react-is@18.2.0: {}
react-router-dom@6.21.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
@@ -8677,15 +8630,8 @@ snapshots:
optionalDependencies:
'@swc/core': 1.3.101
tslib@1.14.1: {}
tslib@2.6.2: {}
tsutils@3.21.0(typescript@5.0.2):
dependencies:
tslib: 1.14.1
typescript: 5.0.2
tty-table@4.2.3:
dependencies:
chalk: 4.1.2