Compare commits
77 Commits
jazz-react
...
jazz-react
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f75dc8d97 | ||
|
|
e2c79cccb5 | ||
|
|
c14a0e05be | ||
|
|
016dd3a5dd | ||
|
|
5c4ca9103c | ||
|
|
b4aad92907 | ||
|
|
56d1e095a1 | ||
|
|
6dee9aae49 | ||
|
|
a10bff981e | ||
|
|
e333f7884a | ||
|
|
8ea7bf237b | ||
|
|
5e8409fa08 | ||
|
|
23354c1767 | ||
|
|
0efb69d0db | ||
|
|
0462c4e41b | ||
|
|
70a5673197 | ||
|
|
9ec3203485 | ||
|
|
1a46f9b2e1 | ||
|
|
77bb26a8d7 | ||
|
|
2a36dcf592 | ||
|
|
fc2bcadbe2 | ||
|
|
46b0cc1adb | ||
|
|
d75d1c6a3f | ||
|
|
13b236aeed | ||
|
|
1c0a61b0b2 | ||
|
|
ceb92438f4 | ||
|
|
9bdd62ed4c | ||
|
|
3f5ef7e799 | ||
|
|
e7a573fa94 | ||
|
|
364060eaa7 | ||
|
|
a3ddc3d5e0 | ||
|
|
185f747adb | ||
|
|
895d281088 | ||
|
|
b44e4354f7 | ||
|
|
3fcb0665ec | ||
|
|
be49d33ce5 | ||
|
|
c7dae1608b | ||
|
|
b020c5868b | ||
|
|
eae42d3afe | ||
|
|
a816e2436e | ||
|
|
b09e35e372 | ||
|
|
d2c8121c9c | ||
|
|
380bb88ffa | ||
|
|
e0e3726b3c | ||
|
|
c2253a7979 | ||
|
|
9d244226ec | ||
|
|
71df5e3a59 | ||
|
|
3a738dad88 | ||
|
|
56d301cfde | ||
|
|
5efec6d5ea | ||
|
|
32769b24f1 | ||
|
|
6ab53c263d | ||
|
|
e7f3e4e242 | ||
|
|
8bb5201647 | ||
|
|
a9fc94f53d | ||
|
|
ca7c0510d1 | ||
|
|
1bf16f0859 | ||
|
|
21b503c188 | ||
|
|
0053e9796c | ||
|
|
e84941b1b1 | ||
|
|
57f6f8d67e | ||
|
|
5b8e69d973 | ||
|
|
7213b1bfa3 | ||
|
|
11f0770f08 | ||
|
|
44e6dc3ae8 | ||
|
|
b5d20d2488 | ||
|
|
0185545838 | ||
|
|
8c8f85859c | ||
|
|
104384409e | ||
|
|
179827ae56 | ||
|
|
6645829876 | ||
|
|
68cb302722 | ||
|
|
8dc33f2790 | ||
|
|
5f64ba326c | ||
|
|
7ccb15107c | ||
|
|
b102964743 | ||
|
|
216d50a09c |
82
.github/workflows/build-and-deploy.yaml
vendored
82
.github/workflows/build-and-deploy.yaml
vendored
@@ -53,53 +53,6 @@ jobs:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
build-homepage:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: gardencmp
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pnpm Install (root)
|
||||
run: |
|
||||
pnpm install
|
||||
working-directory: .
|
||||
|
||||
- name: Pnpm Install & Build (homepage)
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm build;
|
||||
working-directory: ./homepage/homepage
|
||||
|
||||
- name: Docker Build & Push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: ./homepage
|
||||
push: true
|
||||
tags: ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
deploy-examples:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-examples
|
||||
@@ -135,37 +88,4 @@ jobs:
|
||||
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
|
||||
cat job-instance.nomad;
|
||||
NOMAD_ADDR=${{ secrets.NOMAD_ADDR }} nomad job run job-instance.nomad;
|
||||
working-directory: ./examples/${{ matrix.example }}
|
||||
|
||||
deploy-homepage:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-homepage
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
- uses: gacts/install-nomad@v1
|
||||
- name: Tailscale
|
||||
uses: tailscale/github-action@v1
|
||||
with:
|
||||
authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
|
||||
|
||||
- name: Deploy on Nomad
|
||||
run: |
|
||||
if [ "${{github.ref_name}}" == "main" ]; then
|
||||
export BRANCH_SUFFIX="";
|
||||
export BRANCH_SUBDOMAIN="";
|
||||
else
|
||||
export BRANCH_SUFFIX=-${{github.head_ref || github.ref_name}};
|
||||
export BRANCH_SUBDOMAIN=${{github.head_ref || github.ref_name}}.;
|
||||
fi
|
||||
|
||||
export DOCKER_USER=gardencmp;
|
||||
export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
|
||||
export DOCKER_TAG=ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}};
|
||||
|
||||
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
|
||||
cat job-instance.nomad;
|
||||
NOMAD_ADDR=${{ secrets.NOMAD_ADDR }} nomad job run job-instance.nomad;
|
||||
working-directory: ./homepage
|
||||
working-directory: ./examples/${{ matrix.example }}
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
@@ -1,5 +1,93 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 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
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.25
|
||||
- jazz-react@0.7.25
|
||||
|
||||
## 0.0.71
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.24
|
||||
- jazz-react@0.7.24
|
||||
|
||||
## 0.0.70
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.23
|
||||
- jazz-react@0.7.23
|
||||
- jazz-tools@0.7.23
|
||||
|
||||
## 0.0.69
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.7.22
|
||||
|
||||
## 0.0.68
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.21
|
||||
- jazz-react@0.7.21
|
||||
|
||||
## 0.0.67
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.20
|
||||
- jazz-react@0.7.20
|
||||
|
||||
## 0.0.66
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.19
|
||||
- jazz-react@0.7.19
|
||||
|
||||
## 0.0.65
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-chat",
|
||||
"private": true,
|
||||
"version": "0.0.65",
|
||||
"version": "0.0.76",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -3,6 +3,7 @@ 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;
|
||||
@@ -39,4 +40,4 @@ function App() {
|
||||
}
|
||||
|
||||
createRoot(document.getElementById("root")!)
|
||||
.render(<Jazz.Provider><App/></Jazz.Provider>);
|
||||
.render(<StrictMode><Jazz.Provider><App/></Jazz.Provider></StrictMode>);
|
||||
@@ -1,5 +1,51 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 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
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.23
|
||||
- cojson-transport-ws@0.7.23
|
||||
|
||||
## 0.0.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson-transport-ws@0.7.22
|
||||
|
||||
## 0.0.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/jazz-logo.png" />
|
||||
<link rel="stylesheet" href="/src/index.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Jazz Chat Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/app.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/jazz-logo.png" />
|
||||
<link rel="stylesheet" href="/src/index.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Jazz Inspector</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/app.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-inspector",
|
||||
"private": true,
|
||||
"version": "0.0.49",
|
||||
"version": "0.0.55",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -19,7 +19,6 @@
|
||||
"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",
|
||||
|
||||
@@ -1,309 +1,4 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import {
|
||||
RawAccount,
|
||||
CoID,
|
||||
RawCoValue,
|
||||
SessionID,
|
||||
LocalNode,
|
||||
AgentSecret,
|
||||
AccountID,
|
||||
cojsonInternals,
|
||||
WasmCrypto,
|
||||
} from "cojson";
|
||||
import { clsx } from "clsx";
|
||||
import { AccountInfo, CoJsonTree, Tag } from "./cojson-tree";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createWebSocketPeer } from "cojson-transport-ws";
|
||||
import { Effect } from "effect";
|
||||
import App from "./viewer/new-app";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
||||
|
||||
function App() {
|
||||
const [accountID, setAccountID] = useState<CoID<RawAccount>>(
|
||||
localStorage["inspectorAccountID"]
|
||||
);
|
||||
const [accountSecret, setAccountSecret] = useState<AgentSecret>(
|
||||
localStorage["inspectorAccountSecret"]
|
||||
);
|
||||
|
||||
const [coValueId, setCoValueId] = useState<CoID<RawCoValue>>(
|
||||
window.location.hash.slice(2) as CoID<RawCoValue>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("hashchange", () => {
|
||||
setCoValueId(window.location.hash.slice(2) as CoID<RawCoValue>);
|
||||
});
|
||||
});
|
||||
|
||||
const [localNode, setLocalNode] = useState<LocalNode>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountID || !accountSecret) return;
|
||||
WasmCrypto.create().then(async (crypto) => {
|
||||
const wsPeer = await Effect.runPromise(
|
||||
createWebSocketPeer({
|
||||
id: "mesh",
|
||||
websocket: new WebSocket("wss://mesh.jazz.tools"),
|
||||
role: "server",
|
||||
})
|
||||
);
|
||||
const node = await LocalNode.withLoadedAccount({
|
||||
accountID: accountID,
|
||||
accountSecret: accountSecret,
|
||||
sessionID: cojsonInternals.newRandomSessionID(accountID),
|
||||
peersToLoadFrom: [wsPeer],
|
||||
crypto,
|
||||
migration: async () => {
|
||||
console.log("Not running any migration in inspector");
|
||||
},
|
||||
});
|
||||
setLocalNode(node);
|
||||
});
|
||||
}, [accountID, accountSecret]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center w-screen h-screen p-2 gap-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
Account
|
||||
<input
|
||||
className="border p-2 rounded"
|
||||
placeholder="Account ID"
|
||||
value={accountID}
|
||||
onChange={(e) => {
|
||||
setAccountID(e.target.value as AccountID);
|
||||
localStorage["inspectorAccountID"] = e.target.value;
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
className="border p-2 rounded"
|
||||
placeholder="Account Secret"
|
||||
value={accountSecret}
|
||||
onChange={(e) => {
|
||||
setAccountSecret(e.target.value as AgentSecret);
|
||||
localStorage["inspectorAccountSecret"] = e.target.value;
|
||||
}}
|
||||
/>
|
||||
{localNode ? (
|
||||
<AccountInfo accountID={accountID} node={localNode} />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
CoValue ID
|
||||
<input
|
||||
className="border p-2 rounded min-w-[20rem]"
|
||||
placeholder="CoValue ID"
|
||||
value={coValueId}
|
||||
onChange={(e) =>
|
||||
setCoValueId(e.target.value as CoID<RawCoValue>)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{coValueId && localNode ? (
|
||||
<Inspect coValueId={coValueId} node={localNode} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// function ImageCoValue({ value }: { value: ImageDefinition["_shape"] }) {
|
||||
// const keys = Object.keys(value);
|
||||
// const keyIncludingRes = keys.find((key) => key.includes("x"));
|
||||
// const idToResolve = keyIncludingRes
|
||||
// ? value[keyIncludingRes as `${number}x${number}`]
|
||||
// : null;
|
||||
|
||||
// if (!idToResolve) return <div>Can't find image</div>;
|
||||
|
||||
// const [blobURL, setBlobURL] = useState<string>();
|
||||
|
||||
// useEffect(() => {
|
||||
|
||||
// })
|
||||
|
||||
// return (
|
||||
// <img
|
||||
// src={image?.blobURL || value.placeholderDataURL}
|
||||
// alt="placeholder"
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
|
||||
function Inspect({
|
||||
coValueId,
|
||||
node,
|
||||
}: {
|
||||
coValueId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const [coValue, setCoValue] = useState<RawCoValue | "unavailable">();
|
||||
|
||||
useEffect(() => {
|
||||
return node.subscribe(coValueId, (coValue) => {
|
||||
setCoValue(coValue);
|
||||
});
|
||||
}, [node, coValueId]);
|
||||
|
||||
if (coValue === "unavailable") {
|
||||
return <div>Unavailable</div>;
|
||||
}
|
||||
|
||||
const values = coValue?.toJSON() || {};
|
||||
const isImage =
|
||||
typeof values === "object" && "placeholderDataURL" in values;
|
||||
const isGroup = coValue?.core.header.ruleset?.type === "group";
|
||||
|
||||
const entires = Object.entries(values as any) as [string, string][];
|
||||
const onlyCoValues = entires.filter(([key]) => key.startsWith("co_"));
|
||||
|
||||
let title = "";
|
||||
if (isImage) {
|
||||
title = "Image";
|
||||
} else if (isGroup) {
|
||||
title = "Group";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-auto">
|
||||
<h1 className="text-xl font-bold mb-2">
|
||||
Inspecting {title}{" "}
|
||||
<span className="text-gray-500 text-sm">{coValueId}</span>
|
||||
</h1>
|
||||
|
||||
{isGroup ? (
|
||||
<p>
|
||||
{onlyCoValues.length > 0 ? <h3>Permissions</h3> : ""}
|
||||
<div className="flex gap-2 flex-col">
|
||||
{onlyCoValues?.map(([key, value]) => (
|
||||
<div className="flex gap-1 items-center">
|
||||
<span className="bg-gray-200 text-xs px-2 py-0.5 rounded">
|
||||
{value}
|
||||
</span>
|
||||
<AccountInfo
|
||||
accountID={key as CoID<RawAccount>}
|
||||
node={node}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</p>
|
||||
) : (
|
||||
<span className="">
|
||||
Group{" "}
|
||||
<Tag href={`#/${coValue?.group.id}`}>
|
||||
{coValue?.group.id}
|
||||
</Tag>
|
||||
</span>
|
||||
)}
|
||||
{/* {isImage ? (
|
||||
<div className="my-2">
|
||||
<ImageCoValue value={values as any} />
|
||||
</div>
|
||||
) : null} */}
|
||||
<pre className="max-w-[80vw] overflow-scroll text-sm mt-4">
|
||||
<CoJsonTree coValueId={coValueId} node={node} />
|
||||
</pre>
|
||||
<h2 className="text-lg font-semibold mt-10 mb-4">Sessions</h2>
|
||||
{coValue && <Sessions coValue={coValue} node={node} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Sessions({ coValue, node }: { coValue: RawCoValue; node: LocalNode }) {
|
||||
const validTx = coValue.core.getValidSortedTransactions();
|
||||
return (
|
||||
<div className="max-w-[80vw] border rounded">
|
||||
{[...coValue.core.sessionLogs.entries()].map(
|
||||
([sessionID, session]) => (
|
||||
<div
|
||||
key={sessionID}
|
||||
className="mv-10 flex gap-2 border-b p-5 flex-wrap flex-col"
|
||||
>
|
||||
<div className="flex gap-2 flex-row">
|
||||
<SessionInfo
|
||||
sessionID={sessionID}
|
||||
transactionCount={session.transactions.length}
|
||||
node={node}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-1 flex-wrap max-h-64 overflow-y-auto p-1 bg-gray-50 rounded">
|
||||
{session.transactions.map((tx, txIdx) => {
|
||||
const correspondingValidTx = validTx.find(
|
||||
(validTx) =>
|
||||
validTx.txID.sessionID === sessionID &&
|
||||
validTx.txID.txIndex == txIdx
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={txIdx}
|
||||
className={clsx(
|
||||
"text-xs flex-1 p-2 border rounded min-w-36 max-w-40 overflow-scroll bg-white",
|
||||
!correspondingValidTx &&
|
||||
"bg-red-50 border-red-100"
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
{new Date(
|
||||
tx.madeAt
|
||||
).toLocaleString()}
|
||||
</div>
|
||||
<div>{tx.privacy}</div>
|
||||
<pre>
|
||||
{correspondingValidTx
|
||||
? JSON.stringify(
|
||||
correspondingValidTx.changes,
|
||||
undefined,
|
||||
2
|
||||
)
|
||||
: "invalid/undecryptable"}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
{session.lastHash} / {session.lastSignature}{" "}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionInfo({
|
||||
sessionID,
|
||||
transactionCount,
|
||||
node,
|
||||
}: {
|
||||
sessionID: SessionID;
|
||||
transactionCount: number;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
let Prefix = sessionID.startsWith("co_") ? (
|
||||
<AccountInfo
|
||||
accountID={sessionID.split("_session_")[0] as CoID<RawAccount>}
|
||||
node={node}
|
||||
/>
|
||||
) : (
|
||||
<pre className="text-xs">{sessionID.split("_session_")[0]}</pre>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{Prefix}
|
||||
<div>
|
||||
<span className="text-xs">
|
||||
Session {sessionID.split("_session_")[1]}
|
||||
</span>
|
||||
<span className="text-xs text-gray-600 font-medium">
|
||||
{" "}
|
||||
- {transactionCount} txs
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import { AccountID, CoID, LocalNode, RawAccount, RawCoMap, RawCoValue } from "cojson";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LinkIcon } from "./link-icon";
|
||||
|
||||
export function CoJsonTree({
|
||||
coValueId,
|
||||
node,
|
||||
}: {
|
||||
coValueId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const [coValue, setCoValue] = useState<RawCoValue | "unavailable">();
|
||||
|
||||
useEffect(() => {
|
||||
return node.subscribe(coValueId, (value) => {
|
||||
setCoValue(value);
|
||||
});
|
||||
});
|
||||
|
||||
if (coValue === "unavailable") {
|
||||
return <div className="text-red-500">Unavailable</div>;
|
||||
}
|
||||
|
||||
const values = coValue?.toJSON() || {};
|
||||
|
||||
return <RenderCoValueJSON json={values} node={node} />;
|
||||
}
|
||||
|
||||
function RenderObject({
|
||||
json,
|
||||
node,
|
||||
}: {
|
||||
json: Record<string, any>;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const [limit, setLimit] = useState(10);
|
||||
const hasMore = Object.keys(json).length > limit;
|
||||
|
||||
const entries = Object.entries(json).slice(0, limit);
|
||||
return (
|
||||
<div className="flex gap-x-1 flex-col font-mono text-xs overflow-auto">
|
||||
{"{"}
|
||||
{entries.map(([key, value]) => {
|
||||
return (
|
||||
<RenderObjectValue
|
||||
property={key}
|
||||
value={value}
|
||||
node={node}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{hasMore ? (
|
||||
<div
|
||||
className="text-gray-500 cursor-pointer"
|
||||
onClick={() => setLimit((l) => l + 10)}
|
||||
>
|
||||
... {Object.keys(json).length - limit} more
|
||||
</div>
|
||||
) : null}
|
||||
{"}"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderObjectValue({
|
||||
property,
|
||||
value,
|
||||
node,
|
||||
}: {
|
||||
property: string;
|
||||
value: any;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const [shouldLoad, setShouldLoad] = useState(false);
|
||||
|
||||
const isCoValue =
|
||||
typeof value === "string" ? value?.startsWith("co_") : false;
|
||||
|
||||
return (
|
||||
<div className={clsx(`flex group`)}>
|
||||
<div className="text-gray-500 flex items-start">
|
||||
<div className="flex items-center">
|
||||
<RenderCoValueJSON json={property} node={node} />:{" "}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCoValue ? (
|
||||
<div className={clsx(shouldLoad && "pb-2")}>
|
||||
<div className="flex items-center ">
|
||||
<div onClick={() => setShouldLoad((s) => !s)}>
|
||||
<div className="w-8 text-center text-gray-700 font-mono px-1 text-xs rounded hover:bg-gray-300 cursor-pointer">
|
||||
{shouldLoad ? `-` : `...`}
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={`#/${value}`}
|
||||
className="ml-2 group-hover:block hidden"
|
||||
>
|
||||
<LinkIcon />
|
||||
</a>
|
||||
</div>
|
||||
<span>
|
||||
{shouldLoad ? (
|
||||
<CoJsonTree coValueId={value} node={node} />
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="">
|
||||
<RenderCoValueJSON json={value} node={node} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderCoValueArray({ json, node }: { json: any[]; node: LocalNode }) {
|
||||
const [limit, setLimit] = useState(10);
|
||||
const hasMore = json.length > limit;
|
||||
|
||||
const entries = json.slice(0, limit);
|
||||
return (
|
||||
<div className="flex gap-x-1 flex-col font-mono text-xs overflow-auto">
|
||||
{entries.map((value, idx) => {
|
||||
return (
|
||||
<div key={idx} className="flex gap-x-1">
|
||||
<RenderCoValueJSON json={value} node={node} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{hasMore ? (
|
||||
<div
|
||||
className="text-gray-500 cursor-pointer"
|
||||
onClick={() => setLimit((l) => l + 10)}
|
||||
>
|
||||
... {json.length - limit} more
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderCoValueJSON({
|
||||
json,
|
||||
node,
|
||||
}: {
|
||||
json:
|
||||
| Record<string, any>
|
||||
| any[]
|
||||
| string
|
||||
| null
|
||||
| number
|
||||
| boolean
|
||||
| undefined;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
if (typeof json === "undefined") {
|
||||
return <>"undefined"</>;
|
||||
} else if (Array.isArray(json)) {
|
||||
return (
|
||||
<div className="">
|
||||
<span className="text-gray-500">[</span>
|
||||
<div className="ml-2">
|
||||
<RenderCoValueArray json={json} node={node} />
|
||||
</div>
|
||||
<span className="text-gray-500">]</span>
|
||||
</div>
|
||||
);
|
||||
} else if (
|
||||
typeof json === "object" &&
|
||||
json &&
|
||||
Object.getPrototypeOf(json) === Object.prototype
|
||||
) {
|
||||
return <RenderObject json={json} node={node} />;
|
||||
} else if (typeof json === "string") {
|
||||
if (json?.startsWith("co_")) {
|
||||
if (json.includes("_session_")) {
|
||||
return (
|
||||
<>
|
||||
<AccountInfo accountID={json.split("_session_")[0] as AccountID} node={node}/>{" "}
|
||||
(sess {json.split("_session_")[1]})
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<a className="underline" href={`#/${json}`}>
|
||||
{'"'}
|
||||
{json}
|
||||
{'"'}
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return <div className="truncate max-w-64 ml-1">{json}</div>;
|
||||
}
|
||||
} else {
|
||||
return <div className="truncate max-w-64">{JSON.stringify(json)}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export function AccountInfo({ accountID, node }: { accountID: CoID<RawAccount>, node: LocalNode }) {
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const account = await node.load(accountID);
|
||||
if (account === "unavailable") return;
|
||||
const profileID = account?.get("profile");
|
||||
if (profileID === undefined) return;
|
||||
const profile = await node.load(profileID as CoID<RawCoMap>);
|
||||
if (profile === "unavailable") return;
|
||||
setName(profile?.get("name") as string);
|
||||
})()
|
||||
}, [accountID, node]);
|
||||
|
||||
return name ? (
|
||||
<Tag href={`#/${accountID}`} title={accountID}><h1>{name}</h1></Tag>
|
||||
) : (
|
||||
<Tag href={`#/${accountID}`}>{accountID}</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
export function Tag({
|
||||
children,
|
||||
href,
|
||||
title
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
href?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
if (href) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className="border text-xs px-2 py-0.5 rounded hover:underline"
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="border text-xs px-2 py-0.5 rounded">{children}</span>
|
||||
);
|
||||
}
|
||||
@@ -3,76 +3,90 @@
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 20 14.3% 4.1%;
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 20 14.3% 4.1%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 20 14.3% 4.1%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 20 14.3% 4.1%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--primary: 24 9.8% 10%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
--primary: 24 9.8% 10%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--secondary: 60 4.8% 95.9%;
|
||||
--secondary-foreground: 24 9.8% 10%;
|
||||
--secondary: 60 4.8% 95.9%;
|
||||
--secondary-foreground: 24 9.8% 10%;
|
||||
|
||||
--muted: 60 4.8% 95.9%;
|
||||
--muted-foreground: 25 5.3% 44.7%;
|
||||
--muted: 60 4.8% 95.9%;
|
||||
--muted-foreground: 25 5.3% 44.7%;
|
||||
|
||||
--accent: 60 4.8% 95.9%;
|
||||
--accent-foreground: 24 9.8% 10%;
|
||||
--accent: 60 4.8% 95.9%;
|
||||
--accent-foreground: 24 9.8% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 20 5.9% 90%;
|
||||
--input: 20 5.9% 90%;
|
||||
--ring: 20 14.3% 4.1%;
|
||||
--border: 20 5.9% 90%;
|
||||
--input: 20 5.9% 90%;
|
||||
--ring: 20 14.3% 4.1%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 20 14.3% 4.1%;
|
||||
--foreground: 60 9.1% 97.8%;
|
||||
.dark {
|
||||
--background: 20 14.3% 4.1%;
|
||||
--foreground: 60 9.1% 97.8%;
|
||||
|
||||
--card: 20 14.3% 4.1%;
|
||||
--card-foreground: 60 9.1% 97.8%;
|
||||
--card: 20 14.3% 4.1%;
|
||||
--card-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--popover: 20 14.3% 4.1%;
|
||||
--popover-foreground: 60 9.1% 97.8%;
|
||||
--popover: 20 14.3% 4.1%;
|
||||
--popover-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--primary: 60 9.1% 97.8%;
|
||||
--primary-foreground: 24 9.8% 10%;
|
||||
--primary: 60 9.1% 97.8%;
|
||||
--primary-foreground: 24 9.8% 10%;
|
||||
|
||||
--secondary: 12 6.5% 15.1%;
|
||||
--secondary-foreground: 60 9.1% 97.8%;
|
||||
--secondary: 12 6.5% 15.1%;
|
||||
--secondary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--muted: 12 6.5% 15.1%;
|
||||
--muted-foreground: 24 5.4% 63.9%;
|
||||
--muted: 12 6.5% 15.1%;
|
||||
--muted-foreground: 24 5.4% 63.9%;
|
||||
|
||||
--accent: 12 6.5% 15.1%;
|
||||
--accent-foreground: 60 9.1% 97.8%;
|
||||
--accent: 12 6.5% 15.1%;
|
||||
--accent-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 12 6.5% 15.1%;
|
||||
--input: 12 6.5% 15.1%;
|
||||
--ring: 24 5.7% 82.9%;
|
||||
}
|
||||
--border: 12 6.5% 15.1%;
|
||||
--input: 12 6.5% 15.1%;
|
||||
--ring: 24 5.7% 82.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateZ(400px) translateY(30px) scale(1.05);
|
||||
opacity: 0.4;
|
||||
}
|
||||
to {
|
||||
transform: translateZ(0) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
export function LinkIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
42
examples/inspector/src/viewer/breadcrumbs.tsx
Normal file
42
examples/inspector/src/viewer/breadcrumbs.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import { PageInfo } from "./types";
|
||||
|
||||
interface BreadcrumbsProps {
|
||||
path: PageInfo[];
|
||||
onBreadcrumbClick: (index: number) => void;
|
||||
}
|
||||
|
||||
export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
|
||||
path,
|
||||
onBreadcrumbClick,
|
||||
}) => {
|
||||
return (
|
||||
<div className="z-20 relative bg-indigo-400/10 backdrop-blur-sm rounded-lg inline-flex px-2 py-1 whitespace-pre transition-all items-center space-x-1 min-h-10">
|
||||
<button
|
||||
onClick={() => onBreadcrumbClick(-1)}
|
||||
className="flex items-center justify-center p-1 rounded-sm hover:bg-indigo-500/10 transition-colors"
|
||||
aria-label="Go to home"
|
||||
>
|
||||
<img src="jazz-logo.png" alt="Jazz Logo" className="size-5" />
|
||||
</button>
|
||||
{path.map((page, index) => {
|
||||
return (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block first:pl-1 last:pr-1"
|
||||
>
|
||||
{index === 0 ? null : (
|
||||
<span className="text-indigo-500/30">{" / "}</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => onBreadcrumbClick(index)}
|
||||
className="text-indigo-700 hover:underline"
|
||||
>
|
||||
{index === 0 ? page.name || "Root" : page.name}
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
353
examples/inspector/src/viewer/co-stream-view.tsx
Normal file
353
examples/inspector/src/viewer/co-stream-view.tsx
Normal file
@@ -0,0 +1,353 @@
|
||||
import {
|
||||
CoID,
|
||||
LocalNode,
|
||||
RawBinaryCoStream,
|
||||
RawCoStream,
|
||||
RawCoValue,
|
||||
} from "cojson";
|
||||
import { JsonObject, JsonValue } from "cojson/src/jsonValue";
|
||||
import { PageInfo } from "./types";
|
||||
import { base64URLtoBytes } from "cojson/src/base64url";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ArrowDownToLine } from "lucide-react";
|
||||
import {
|
||||
BinaryStreamItem,
|
||||
BinaryStreamStart,
|
||||
CoStreamItem,
|
||||
} from "cojson/src/coValues/coStream";
|
||||
import { AccountOrGroupPreview } from "./value-renderer";
|
||||
|
||||
// typeguard for BinaryStreamStart
|
||||
function isBinaryStreamStart(item: unknown): item is BinaryStreamStart {
|
||||
return (
|
||||
typeof item === "object" &&
|
||||
item !== null &&
|
||||
"type" in item &&
|
||||
item.type === "start"
|
||||
);
|
||||
}
|
||||
|
||||
function detectCoStreamType(value: RawCoStream | RawBinaryCoStream) {
|
||||
const firstKey = Object.keys(value.items)[0];
|
||||
if (!firstKey)
|
||||
return {
|
||||
type: "unknown",
|
||||
};
|
||||
|
||||
const items = value.items[firstKey as never]?.map((v) => v.value);
|
||||
|
||||
if (!items)
|
||||
return {
|
||||
type: "unknown",
|
||||
};
|
||||
const firstItem = items[0];
|
||||
if (!firstItem)
|
||||
return {
|
||||
type: "unknown",
|
||||
};
|
||||
// This is a binary stream
|
||||
if (isBinaryStreamStart(firstItem)) {
|
||||
return {
|
||||
type: "binary",
|
||||
items: items as BinaryStreamItem[],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "coStream",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getBlobFromCoStream({
|
||||
items,
|
||||
onlyFirstChunk = false,
|
||||
}: {
|
||||
items: BinaryStreamItem[];
|
||||
onlyFirstChunk?: boolean;
|
||||
}) {
|
||||
if (onlyFirstChunk && items.length > 1) {
|
||||
items = items.slice(0, 2);
|
||||
}
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
|
||||
const binary_U_prefixLength = 8;
|
||||
|
||||
let lastProgressUpdate = Date.now();
|
||||
|
||||
for (const item of items.slice(1)) {
|
||||
if (item.type === "end") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (item.type !== "chunk") {
|
||||
console.error("Invalid binary stream chunk", item);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const chunk = base64URLtoBytes(item.chunk.slice(binary_U_prefixLength));
|
||||
// totalLength += chunk.length;
|
||||
chunks.push(chunk);
|
||||
|
||||
if (Date.now() - lastProgressUpdate > 100) {
|
||||
lastProgressUpdate = Date.now();
|
||||
}
|
||||
}
|
||||
const defaultMime = "mimeType" in items[0] ? items[0].mimeType : null;
|
||||
|
||||
const blob = new Blob(chunks, defaultMime ? { type: defaultMime } : {});
|
||||
|
||||
const mimeType =
|
||||
defaultMime === "" ? await detectPDFMimeType(blob) : defaultMime;
|
||||
|
||||
return {
|
||||
blob,
|
||||
mimeType: mimeType as string,
|
||||
unfinishedChunks: items.length > 1,
|
||||
totalSize:
|
||||
"totalSizeBytes" in items[0]
|
||||
? (items[0].totalSizeBytes as number)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const detectPDFMimeType = async (blob: Blob): Promise<string> => {
|
||||
const arrayBuffer = await blob.slice(0, 4).arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
const header = uint8Array.reduce(
|
||||
(acc, byte) => acc + String.fromCharCode(byte),
|
||||
"",
|
||||
);
|
||||
|
||||
if (header === "%PDF") {
|
||||
return "application/pdf";
|
||||
}
|
||||
return "application/octet-stream";
|
||||
};
|
||||
|
||||
const BinaryDownloadButton = ({
|
||||
pdfBlob,
|
||||
fileName = "document",
|
||||
label,
|
||||
mimeType,
|
||||
}: {
|
||||
pdfBlob: Blob;
|
||||
mimeType?: string;
|
||||
fileName?: string;
|
||||
label: string;
|
||||
}) => {
|
||||
const downloadFile = () => {
|
||||
const url = URL.createObjectURL(
|
||||
new Blob([pdfBlob], mimeType ? { type: mimeType } : {}),
|
||||
);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download =
|
||||
mimeType === "application/pdf" ? `${fileName}.pdf` : fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className="flex items-center gap-2 px-2 py-1 text-gray-900 border border-gray-900/10 bg-clip-border shadow-sm transition-colors rounded bg-gray-50 text-sm"
|
||||
onClick={downloadFile}
|
||||
>
|
||||
<ArrowDownToLine size={16} />
|
||||
{label}
|
||||
{/* Download {mimeType === "application/pdf" ? "PDF" : "File"} */}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const LabelContentPair = ({
|
||||
label,
|
||||
content,
|
||||
}: {
|
||||
label: string;
|
||||
content: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5 ">
|
||||
<span className="uppercase text-xs font-medium text-gray-600 tracking-wide">
|
||||
{label}
|
||||
</span>
|
||||
<span>{content}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function RenderCoBinaryStream({
|
||||
value,
|
||||
items,
|
||||
}: {
|
||||
items: BinaryStreamItem[];
|
||||
value: RawBinaryCoStream;
|
||||
}) {
|
||||
const [file, setFile] = useState<
|
||||
| {
|
||||
blob: Blob;
|
||||
mimeType: string;
|
||||
unfinishedChunks: boolean;
|
||||
totalSize: number | undefined;
|
||||
}
|
||||
| undefined
|
||||
| null
|
||||
>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// load only the first chunk to get the mime type and size
|
||||
getBlobFromCoStream({
|
||||
items,
|
||||
onlyFirstChunk: true,
|
||||
})
|
||||
.then((v) => {
|
||||
if (v) {
|
||||
setFile(v);
|
||||
if (v.mimeType.includes("image")) {
|
||||
// If it's an image, load the full blob
|
||||
getBlobFromCoStream({
|
||||
items,
|
||||
}).then((s) => {
|
||||
if (s) setFile(s);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [items]);
|
||||
|
||||
if (!isLoading && !file) return <div>No blob</div>;
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (!file) return <div>No blob</div>;
|
||||
|
||||
const { blob, mimeType } = file;
|
||||
|
||||
const sizeInKB = (file.totalSize || 0) / 1024;
|
||||
|
||||
return (
|
||||
<div className="space-y-8 mt-4">
|
||||
<div className="grid grid-cols-3 gap-2 max-w-3xl">
|
||||
<LabelContentPair
|
||||
label="Mime Type"
|
||||
content={
|
||||
<span className="font-mono bg-gray-100 rounded px-2 py-1 text-sm">
|
||||
{mimeType || "No mime type"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<LabelContentPair
|
||||
label="Size"
|
||||
content={<span>{sizeInKB.toFixed(2)} KB</span>}
|
||||
/>
|
||||
<LabelContentPair
|
||||
label="Download"
|
||||
content={
|
||||
<BinaryDownloadButton
|
||||
fileName={value.id.toString()}
|
||||
pdfBlob={blob}
|
||||
mimeType={mimeType}
|
||||
label={
|
||||
mimeType === "application/pdf"
|
||||
? "Download PDF"
|
||||
: "Download File"
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{mimeType === "image/png" || mimeType === "image/jpeg" ? (
|
||||
<LabelContentPair
|
||||
label="Preview"
|
||||
content={
|
||||
<div className="bg-gray-50 p-3 rounded-sm">
|
||||
<RenderBlobImage blob={blob} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderCoStream({
|
||||
value,
|
||||
node,
|
||||
}: {
|
||||
value: RawCoStream;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const streamPerUser = Object.keys(value.items);
|
||||
const userCoIds = streamPerUser.map(
|
||||
(stream) => stream.split("_session")[0],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{userCoIds.map((id, idx) => (
|
||||
<div
|
||||
className="bg-gray-100 p-3 rounded-lg transition-colors overflow-hidden bg-white border hover:bg-gray-100/5 cursor-pointer shadow-sm"
|
||||
key={id}
|
||||
>
|
||||
<AccountOrGroupPreview
|
||||
coId={id as CoID<RawCoValue>}
|
||||
node={node}
|
||||
/>
|
||||
{/* @ts-expect-error - TODO: fix types */}
|
||||
{value.items[streamPerUser[idx]]?.map(
|
||||
(item: CoStreamItem<JsonValue>) => (
|
||||
<div>
|
||||
{new Date(item.madeAt).toLocaleString()}{" "}
|
||||
{JSON.stringify(item.value)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CoStreamView({
|
||||
value,
|
||||
node,
|
||||
}: {
|
||||
data: JsonObject;
|
||||
onNavigate: (pages: PageInfo[]) => void;
|
||||
node: LocalNode;
|
||||
value: RawCoStream;
|
||||
}) {
|
||||
// if (!value) return <div>No value</div>;
|
||||
|
||||
const streamType = detectCoStreamType(value);
|
||||
|
||||
if (streamType.type === "binary") {
|
||||
if (streamType.items === undefined) {
|
||||
return <div>No binary stream</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderCoBinaryStream
|
||||
value={value as RawBinaryCoStream}
|
||||
items={streamType.items}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (streamType.type === "coStream") {
|
||||
return <RenderCoStream value={value} node={node} />;
|
||||
}
|
||||
|
||||
if (streamType.type === "unknown") return <div>Unknown stream type</div>;
|
||||
|
||||
return <div>Unknown stream type</div>;
|
||||
}
|
||||
|
||||
function RenderBlobImage({ blob }: { blob: Blob }) {
|
||||
const urlCreator = window.URL || window.webkitURL;
|
||||
return <img src={urlCreator.createObjectURL(blob)} />;
|
||||
}
|
||||
73
examples/inspector/src/viewer/grid-view.tsx
Normal file
73
examples/inspector/src/viewer/grid-view.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
import { JsonObject } from "cojson/src/jsonValue";
|
||||
import { CoMapPreview, ValueRenderer } from "./value-renderer";
|
||||
import clsx from "clsx";
|
||||
import { PageInfo, isCoId } from "./types";
|
||||
import { ResolveIcon } from "./type-icon";
|
||||
|
||||
export function GridView({
|
||||
data,
|
||||
onNavigate,
|
||||
node,
|
||||
}: {
|
||||
data: JsonObject;
|
||||
onNavigate: (pages: PageInfo[]) => void;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const entries = Object.entries(data);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-2">
|
||||
{entries.map(([key, child], childIndex) => (
|
||||
<div
|
||||
key={childIndex}
|
||||
className={clsx(
|
||||
"bg-gray-100 p-3 rounded-lg transition-colors overflow-hidden",
|
||||
isCoId(child)
|
||||
? "bg-white border hover:bg-gray-100/5 cursor-pointer shadow-sm"
|
||||
: "bg-gray-50",
|
||||
)}
|
||||
onClick={() =>
|
||||
isCoId(child) &&
|
||||
onNavigate([
|
||||
{ coId: child as CoID<RawCoValue>, name: key },
|
||||
])
|
||||
}
|
||||
>
|
||||
<h3 className="truncate">
|
||||
{isCoId(child) ? (
|
||||
<span className="font-medium flex justify-between">
|
||||
{key}
|
||||
|
||||
<div className="px-2 py-1 text-xs bg-gray-100 rounded">
|
||||
<ResolveIcon
|
||||
coId={child as CoID<RawCoValue>}
|
||||
node={node}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
) : (
|
||||
<span>{key}</span>
|
||||
)}
|
||||
</h3>
|
||||
<div className="mt-2 text-sm">
|
||||
{isCoId(child) ? (
|
||||
<CoMapPreview
|
||||
coId={child as CoID<RawCoValue>}
|
||||
node={node}
|
||||
/>
|
||||
) : (
|
||||
<ValueRenderer
|
||||
json={child}
|
||||
onCoIDClick={(coId) => {
|
||||
onNavigate([{ coId, name: key }]);
|
||||
}}
|
||||
compact
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
examples/inspector/src/viewer/index.tsx
Normal file
27
examples/inspector/src/viewer/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { LocalNode } from "cojson";
|
||||
import { Breadcrumbs } from "./breadcrumbs";
|
||||
import { usePagePath } from "./use-page-path";
|
||||
import { PageInfo } from "./types";
|
||||
import { PageStack } from "./page-stack";
|
||||
|
||||
export default function CoJsonViewer({
|
||||
defaultPath,
|
||||
node,
|
||||
}: {
|
||||
defaultPath?: PageInfo[];
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const { path, addPages, goToIndex, goBack } = usePagePath(defaultPath);
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen bg-gray-100 p-4 overflow-hidden">
|
||||
<Breadcrumbs path={path} onBreadcrumbClick={goToIndex} />
|
||||
<PageStack
|
||||
path={path}
|
||||
node={node}
|
||||
goBack={goBack}
|
||||
addPages={addPages}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
310
examples/inspector/src/viewer/new-app.tsx
Normal file
310
examples/inspector/src/viewer/new-app.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
LocalNode,
|
||||
CoID,
|
||||
RawCoValue,
|
||||
RawAccount,
|
||||
AgentSecret,
|
||||
AccountID,
|
||||
cojsonInternals,
|
||||
WasmCrypto,
|
||||
} from "cojson";
|
||||
import { createWebSocketPeer } from "cojson-transport-ws";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { Breadcrumbs } from "./breadcrumbs";
|
||||
import { usePagePath } from "./use-page-path";
|
||||
import { PageStack } from "./page-stack";
|
||||
import { resolveCoValue, useResolvedCoValue } from "./use-resolve-covalue";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface Account {
|
||||
id: CoID<RawAccount>;
|
||||
secret: AgentSecret;
|
||||
}
|
||||
|
||||
export default function CoJsonViewerApp() {
|
||||
const [accounts, setAccounts] = useState<Account[]>(() => {
|
||||
const storedAccounts = localStorage.getItem("inspectorAccounts");
|
||||
return storedAccounts ? JSON.parse(storedAccounts) : [];
|
||||
});
|
||||
const [currentAccount, setCurrentAccount] = useState<Account | null>(() => {
|
||||
const lastSelectedId = localStorage.getItem("lastSelectedAccountId");
|
||||
if (lastSelectedId) {
|
||||
const lastAccount = accounts.find(
|
||||
(account) => account.id === lastSelectedId,
|
||||
);
|
||||
return lastAccount || null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const [localNode, setLocalNode] = useState<LocalNode | null>(null);
|
||||
const [coValueId, setCoValueId] = useState<CoID<RawCoValue> | "">("");
|
||||
const { path, addPages, goToIndex, goBack, setPage } = usePagePath();
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("inspectorAccounts", JSON.stringify(accounts));
|
||||
}, [accounts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentAccount) {
|
||||
localStorage.setItem("lastSelectedAccountId", currentAccount.id);
|
||||
} else {
|
||||
localStorage.removeItem("lastSelectedAccountId");
|
||||
}
|
||||
}, [currentAccount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentAccount) {
|
||||
setLocalNode(null);
|
||||
goToIndex(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
WasmCrypto.create().then(async (crypto) => {
|
||||
const wsPeer = createWebSocketPeer({
|
||||
id: "mesh",
|
||||
websocket: new WebSocket("wss://mesh.jazz.tools"),
|
||||
role: "server",
|
||||
});
|
||||
const node = await LocalNode.withLoadedAccount({
|
||||
accountID: currentAccount.id,
|
||||
accountSecret: currentAccount.secret,
|
||||
sessionID: cojsonInternals.newRandomSessionID(
|
||||
currentAccount.id,
|
||||
),
|
||||
peersToLoadFrom: [wsPeer],
|
||||
crypto,
|
||||
migration: async () => {
|
||||
console.log("Not running any migration in inspector");
|
||||
},
|
||||
});
|
||||
setLocalNode(node);
|
||||
});
|
||||
}, [currentAccount, goToIndex]);
|
||||
|
||||
const addAccount = (id: AccountID, secret: AgentSecret) => {
|
||||
const newAccount = { id, secret };
|
||||
setAccounts([...accounts, newAccount]);
|
||||
setCurrentAccount(newAccount);
|
||||
};
|
||||
|
||||
const deleteCurrentAccount = () => {
|
||||
if (currentAccount) {
|
||||
const updatedAccounts = accounts.filter(
|
||||
(account) => account.id !== currentAccount.id,
|
||||
);
|
||||
setAccounts(updatedAccounts);
|
||||
setCurrentAccount(
|
||||
updatedAccounts.length > 0 ? updatedAccounts[0] : null,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCoValueIdSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (coValueId) {
|
||||
setPage(coValueId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen bg-gray-100 p-4 overflow-hidden flex flex-col">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Breadcrumbs path={path} onBreadcrumbClick={goToIndex} />
|
||||
<AccountSwitcher
|
||||
accounts={accounts}
|
||||
currentAccount={currentAccount}
|
||||
setCurrentAccount={setCurrentAccount}
|
||||
deleteCurrentAccount={deleteCurrentAccount}
|
||||
localNode={localNode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PageStack
|
||||
path={path}
|
||||
node={localNode}
|
||||
goBack={goBack}
|
||||
addPages={addPages}
|
||||
>
|
||||
{!currentAccount ? (
|
||||
<AddAccountForm addAccount={addAccount} />
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleCoValueIdSubmit}
|
||||
aria-hidden={path.length !== 0}
|
||||
className={clsx(
|
||||
"flex flex-col justify-center items-center gap-2 h-full w-full mb-20 ",
|
||||
"transition-all duration-150",
|
||||
path.length > 0
|
||||
? "opacity-0 -translate-y-2 scale-95"
|
||||
: "opacity-100",
|
||||
)}
|
||||
>
|
||||
<fieldset className="flex flex-col gap-2 text-sm">
|
||||
<h2 className="text-3xl font-medium text-gray-950 text-center mb-4">
|
||||
Jazz CoValue Inspector
|
||||
</h2>
|
||||
<input
|
||||
className="border p-4 rounded-lg min-w-[21rem] font-mono"
|
||||
placeholder="co_z1234567890abcdef123456789"
|
||||
value={coValueId}
|
||||
onChange={(e) =>
|
||||
setCoValueId(
|
||||
e.target.value as CoID<RawCoValue>,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-indigo-500 hover:bg-indigo-500/80 text-white px-4 py-2 rounded-md"
|
||||
>
|
||||
Inspect
|
||||
</button>
|
||||
<hr />
|
||||
<button
|
||||
type="button"
|
||||
className="border inline-block px-2 py-1.5 text-black rounded"
|
||||
onClick={() => {
|
||||
setCoValueId(currentAccount.id);
|
||||
setPage(currentAccount.id);
|
||||
}}
|
||||
>
|
||||
Inspect My Account
|
||||
</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
)}
|
||||
</PageStack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountSwitcher({
|
||||
accounts,
|
||||
currentAccount,
|
||||
setCurrentAccount,
|
||||
deleteCurrentAccount,
|
||||
localNode,
|
||||
}: {
|
||||
accounts: Account[];
|
||||
currentAccount: Account | null;
|
||||
setCurrentAccount: (account: Account | null) => void;
|
||||
deleteCurrentAccount: () => void;
|
||||
localNode: LocalNode | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative flex items-center gap-1">
|
||||
<select
|
||||
value={currentAccount?.id || "add-account"}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "add-account") {
|
||||
setCurrentAccount(null);
|
||||
} else {
|
||||
const account = accounts.find(
|
||||
(a) => a.id === e.target.value,
|
||||
);
|
||||
setCurrentAccount(account || null);
|
||||
}
|
||||
}}
|
||||
className="p-2 px-4 bg-gray-100/50 border border-indigo-500/10 backdrop-blur-sm rounded-md text-indigo-700 appearance-none"
|
||||
>
|
||||
{accounts.map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
{localNode ? (
|
||||
<AccountNameDisplay
|
||||
accountId={account.id}
|
||||
node={localNode}
|
||||
/>
|
||||
) : (
|
||||
account.id
|
||||
)}
|
||||
</option>
|
||||
))}
|
||||
<option value="add-account">Add account</option>
|
||||
</select>
|
||||
{currentAccount && (
|
||||
<button
|
||||
onClick={deleteCurrentAccount}
|
||||
className="p-3 rounded hover:bg-gray-200 transition-colors"
|
||||
title="Delete Account"
|
||||
>
|
||||
<Trash2 size={16} className="text-gray-500" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AddAccountForm({
|
||||
addAccount,
|
||||
}: {
|
||||
addAccount: (id: AccountID, secret: AgentSecret) => void;
|
||||
}) {
|
||||
const [id, setId] = useState("");
|
||||
const [secret, setSecret] = useState("");
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
addAccount(id as AccountID, secret as AgentSecret);
|
||||
setId("");
|
||||
setSecret("");
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-col gap-2 max-w-md mx-auto h-full justify-center"
|
||||
>
|
||||
<h2 className="text-2xl font-medium text-gray-900 mb-3">
|
||||
Add an Account to Inspect
|
||||
</h2>
|
||||
<input
|
||||
className="border py-2 px-3 rounded-md"
|
||||
placeholder="Account ID"
|
||||
value={id}
|
||||
onChange={(e) => setId(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
className="border py-2 px-3 rounded-md"
|
||||
placeholder="Account Secret"
|
||||
value={secret}
|
||||
onChange={(e) => setSecret(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-indigo-500 text-white px-4 py-2 rounded-md"
|
||||
>
|
||||
Add Account
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountNameDisplay({
|
||||
accountId,
|
||||
node,
|
||||
}: {
|
||||
accountId: CoID<RawAccount>;
|
||||
node: LocalNode;
|
||||
}) {
|
||||
const { snapshot } = useResolvedCoValue(accountId, node);
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (snapshot && typeof snapshot === "object" && "profile" in snapshot) {
|
||||
const profileId = snapshot.profile as CoID<RawCoValue>;
|
||||
resolveCoValue(profileId, node).then((profileResult) => {
|
||||
if (
|
||||
profileResult.snapshot &&
|
||||
typeof profileResult.snapshot === "object" &&
|
||||
"name" in profileResult.snapshot
|
||||
) {
|
||||
setName(profileResult.snapshot.name as string);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [snapshot, node]);
|
||||
|
||||
return name ? `${name} <${accountId}>` : accountId;
|
||||
}
|
||||
55
examples/inspector/src/viewer/page-stack.tsx
Normal file
55
examples/inspector/src/viewer/page-stack.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Page } from "./page"; // Assuming you have a Page component
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
|
||||
// Define the structure of a page in the path
|
||||
interface PageInfo {
|
||||
coId: CoID<RawCoValue>;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
// Props for the PageStack component
|
||||
interface PageStackProps {
|
||||
path: PageInfo[];
|
||||
node?: LocalNode | null;
|
||||
goBack: () => void;
|
||||
addPages: (pages: PageInfo[]) => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function PageStack({
|
||||
path,
|
||||
node,
|
||||
goBack,
|
||||
addPages,
|
||||
children,
|
||||
}: PageStackProps) {
|
||||
return (
|
||||
<div className="relative mt-4 h-[calc(100vh-6rem)]">
|
||||
{children && (
|
||||
<div className="absolute inset-0 pb-20">{children}</div>
|
||||
)}
|
||||
{node &&
|
||||
path.map((page, index) => (
|
||||
<Page
|
||||
key={`${page.coId}-${index}`}
|
||||
coId={page.coId}
|
||||
node={node}
|
||||
name={page.name || page.coId}
|
||||
onHeaderClick={goBack}
|
||||
onNavigate={addPages}
|
||||
isTopLevel={index === path.length - 1}
|
||||
style={{
|
||||
transform: `translateZ(${(index - path.length + 1) * 200}px) scale(${
|
||||
1 - (path.length - index - 1) * 0.05
|
||||
}) translateY(${-(index - path.length + 1) * -4}%)`,
|
||||
opacity: 1 - (path.length - index - 1) * 0.05,
|
||||
zIndex: index,
|
||||
transitionProperty: "transform, opacity",
|
||||
transitionDuration: "0.3s",
|
||||
transitionTimingFunction: "ease-out",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
154
examples/inspector/src/viewer/page.tsx
Normal file
154
examples/inspector/src/viewer/page.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import clsx from "clsx";
|
||||
import { CoID, LocalNode, RawCoStream, RawCoValue } from "cojson";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useResolvedCoValue } from "./use-resolve-covalue";
|
||||
import { GridView } from "./grid-view";
|
||||
import { PageInfo } from "./types";
|
||||
import { TableView } from "./table-viewer";
|
||||
import { TypeIcon } from "./type-icon";
|
||||
import { CoStreamView } from "./co-stream-view";
|
||||
import { AccountOrGroupPreview } from "./value-renderer";
|
||||
|
||||
type PageProps = {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
name: string;
|
||||
onNavigate: (newPages: PageInfo[]) => void;
|
||||
onHeaderClick?: () => void;
|
||||
isTopLevel?: boolean;
|
||||
style: React.CSSProperties;
|
||||
};
|
||||
|
||||
export function Page({
|
||||
coId,
|
||||
node,
|
||||
name,
|
||||
onNavigate,
|
||||
onHeaderClick,
|
||||
style,
|
||||
isTopLevel,
|
||||
}: PageProps) {
|
||||
const { value, snapshot, type, extendedType } = useResolvedCoValue(
|
||||
coId,
|
||||
node,
|
||||
);
|
||||
const [viewMode, setViewMode] = useState<"grid" | "table">("grid");
|
||||
|
||||
const supportsTableView = type === "colist" || extendedType === "record";
|
||||
|
||||
// Automatically switch to table view if the page is a CoMap record
|
||||
useEffect(() => {
|
||||
if (supportsTableView) {
|
||||
setViewMode("table");
|
||||
}
|
||||
}, [supportsTableView]);
|
||||
|
||||
if (snapshot === "unavailable") {
|
||||
return <div style={style}>Data unavailable</div>;
|
||||
}
|
||||
|
||||
if (!snapshot) {
|
||||
return <div style={style}></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={clsx(
|
||||
"absolute inset-0 border border-gray-900/5 bg-clip-padding bg-white rounded-xl shadow-lg p-6 animate-in",
|
||||
)}
|
||||
>
|
||||
{!isTopLevel && (
|
||||
<div
|
||||
className="absolute inset-x-0 top-0 h-10"
|
||||
aria-label="Back"
|
||||
onClick={() => {
|
||||
onHeaderClick?.();
|
||||
}}
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
)}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-2xl font-bold flex items-start flex-col gap-1">
|
||||
<span>
|
||||
{name}
|
||||
{typeof snapshot === "object" &&
|
||||
"name" in snapshot ? (
|
||||
<span className="text-gray-600 font-medium">
|
||||
{" "}
|
||||
{
|
||||
(
|
||||
snapshot as {
|
||||
name: string;
|
||||
}
|
||||
).name
|
||||
}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono">
|
||||
{type && (
|
||||
<TypeIcon
|
||||
type={type}
|
||||
extendedType={extendedType}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-xs text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono">
|
||||
{coId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* {supportsTableView && (
|
||||
<button
|
||||
onClick={toggleViewMode}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
{viewMode === "grid" ? "Table View" : "Grid View"}
|
||||
</button>
|
||||
)} */}
|
||||
</div>
|
||||
<div className="overflow-auto max-h-[calc(100%-4rem)]">
|
||||
{type === "costream" ? (
|
||||
<CoStreamView
|
||||
data={snapshot}
|
||||
onNavigate={onNavigate}
|
||||
node={node}
|
||||
value={value as RawCoStream}
|
||||
/>
|
||||
) : viewMode === "grid" ? (
|
||||
<GridView
|
||||
data={snapshot}
|
||||
onNavigate={onNavigate}
|
||||
node={node}
|
||||
/>
|
||||
) : (
|
||||
<TableView
|
||||
data={snapshot}
|
||||
node={node}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
)}
|
||||
{/* --- */}
|
||||
{extendedType !== "account" && extendedType !== "group" && (
|
||||
<div className="text-xs text-gray-500 mt-4">
|
||||
Owned by{" "}
|
||||
<AccountOrGroupPreview
|
||||
coId={value.group.id}
|
||||
node={node}
|
||||
showId
|
||||
onClick={() => {
|
||||
onNavigate([
|
||||
{ coId: value.group.id, name: "owner" },
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
142
examples/inspector/src/viewer/table-viewer.tsx
Normal file
142
examples/inspector/src/viewer/table-viewer.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
import { JsonObject } from "cojson/src/jsonValue";
|
||||
import { PageInfo } from "./types";
|
||||
import { useMemo, useState } from "react";
|
||||
import { ValueRenderer } from "./value-renderer";
|
||||
import { LinkIcon } from "../link-icon";
|
||||
import { useResolvedCoValues } from "./use-resolve-covalue";
|
||||
|
||||
export function TableView({
|
||||
data,
|
||||
node,
|
||||
onNavigate,
|
||||
}: {
|
||||
data: JsonObject;
|
||||
node: LocalNode;
|
||||
onNavigate: (pages: PageInfo[]) => void;
|
||||
}) {
|
||||
const [visibleRowsCount, setVisibleRowsCount] = useState(10);
|
||||
const [coIdArray, visibleRows] = useMemo(() => {
|
||||
const coIdArray = Array.isArray(data)
|
||||
? data
|
||||
: Object.values(data).every(
|
||||
(k) => typeof k === "string" && k.startsWith("co_"),
|
||||
)
|
||||
? Object.values(data).map((k) => k as CoID<RawCoValue>)
|
||||
: [];
|
||||
|
||||
const visibleRows = coIdArray.slice(0, visibleRowsCount);
|
||||
|
||||
return [coIdArray, visibleRows];
|
||||
}, [data, visibleRowsCount]);
|
||||
const resolvedRows = useResolvedCoValues(visibleRows, node);
|
||||
|
||||
const hasMore = visibleRowsCount < coIdArray.length;
|
||||
|
||||
if (!coIdArray.length) {
|
||||
return <div>No data to display</div>;
|
||||
}
|
||||
|
||||
if (resolvedRows.length === 0) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const keys = Array.from(
|
||||
new Set(
|
||||
resolvedRows.flatMap((item) => Object.keys(item.snapshot || {})),
|
||||
),
|
||||
);
|
||||
|
||||
const loadMore = () => {
|
||||
setVisibleRowsCount((prevVisibleRows) => prevVisibleRows + 10);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="sticky top-0 border-b">
|
||||
<tr>
|
||||
{["", ...keys].map((key) => (
|
||||
<th
|
||||
key={key}
|
||||
className="px-4 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 rounded"
|
||||
>
|
||||
{key}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{resolvedRows
|
||||
.slice(0, visibleRowsCount)
|
||||
.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className="px-1 py-0">
|
||||
<button
|
||||
onClick={() =>
|
||||
onNavigate([
|
||||
{
|
||||
coId: item.value!.id,
|
||||
name: index.toString(),
|
||||
},
|
||||
])
|
||||
}
|
||||
className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 hover:text-blue-500 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<LinkIcon />
|
||||
</button>
|
||||
</td>
|
||||
{keys.map((key) => (
|
||||
<td
|
||||
key={key}
|
||||
className="px-4 py-4 whitespace-nowrap text-sm text-gray-500"
|
||||
>
|
||||
<ValueRenderer
|
||||
json={
|
||||
(item.snapshot as JsonObject)[
|
||||
key
|
||||
]
|
||||
}
|
||||
onCoIDClick={(coId) => {
|
||||
async function handleClick() {
|
||||
onNavigate([
|
||||
{
|
||||
coId: item.value!
|
||||
.id,
|
||||
name: index.toString(),
|
||||
},
|
||||
{
|
||||
coId: coId,
|
||||
name: key,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
handleClick();
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="py-4 text-gray-500 flex items-center justify-between gap-2">
|
||||
<span>
|
||||
Showing {Math.min(visibleRowsCount, coIdArray.length)} of{" "}
|
||||
{coIdArray.length}
|
||||
</span>
|
||||
{hasMore && (
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={loadMore}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
examples/inspector/src/viewer/type-icon.tsx
Normal file
47
examples/inspector/src/viewer/type-icon.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { CoID, LocalNode, RawCoValue } from "cojson";
|
||||
import {
|
||||
CoJsonType,
|
||||
ExtendedCoJsonType,
|
||||
useResolvedCoValue,
|
||||
} from "./use-resolve-covalue";
|
||||
|
||||
export const TypeIcon = ({
|
||||
type,
|
||||
extendedType,
|
||||
}: {
|
||||
type: CoJsonType;
|
||||
extendedType?: ExtendedCoJsonType;
|
||||
}) => {
|
||||
const iconMap: Record<ExtendedCoJsonType | CoJsonType, string> = {
|
||||
record: "{} Record",
|
||||
image: "🖼️ Image",
|
||||
comap: "{} CoMap",
|
||||
costream: "≋ CoStream",
|
||||
colist: "☰ CoList",
|
||||
account: "👤 Account",
|
||||
group: "👥 Group",
|
||||
};
|
||||
|
||||
const iconKey = extendedType || type;
|
||||
const icon = iconMap[iconKey as keyof typeof iconMap];
|
||||
|
||||
return icon ? <span className="font-mono">{icon}</span> : null;
|
||||
};
|
||||
|
||||
export const ResolveIcon = ({
|
||||
coId,
|
||||
node,
|
||||
}: {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
}) => {
|
||||
const { type, extendedType, snapshot } = useResolvedCoValue(coId, node);
|
||||
|
||||
if (snapshot === "unavailable" && !type) {
|
||||
return <div className="text-gray-600 font-medium">Unavailable</div>;
|
||||
}
|
||||
|
||||
if (!type) return <div className="whitespace-pre w-14 font-mono"> </div>;
|
||||
|
||||
return <TypeIcon type={type} extendedType={extendedType} />;
|
||||
};
|
||||
9
examples/inspector/src/viewer/types.ts
Normal file
9
examples/inspector/src/viewer/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { CoID, RawCoValue } from "cojson";
|
||||
|
||||
export type PageInfo = {
|
||||
coId: CoID<RawCoValue>;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export const isCoId = (coId: unknown): coId is CoID<RawCoValue> =>
|
||||
typeof coId === "string" && coId.startsWith("co_");
|
||||
107
examples/inspector/src/viewer/use-page-path.ts
Normal file
107
examples/inspector/src/viewer/use-page-path.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { PageInfo } from "./types";
|
||||
import { CoID, RawCoValue } from "cojson";
|
||||
|
||||
export function usePagePath(defaultPath?: PageInfo[]) {
|
||||
const [path, setPath] = useState<PageInfo[]>(() => {
|
||||
const hash = window.location.hash.slice(2); // Remove '#/'
|
||||
if (hash) {
|
||||
try {
|
||||
return decodePathFromHash(hash);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse hash:", e);
|
||||
}
|
||||
}
|
||||
return defaultPath || [];
|
||||
});
|
||||
|
||||
const updatePath = useCallback((newPath: PageInfo[]) => {
|
||||
setPath(newPath);
|
||||
const hash = encodePathToHash(newPath);
|
||||
window.location.hash = `#/${hash}`;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleHashChange = () => {
|
||||
const hash = window.location.hash.slice(2);
|
||||
if (hash) {
|
||||
try {
|
||||
const newPath = decodePathFromHash(hash);
|
||||
setPath(newPath);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse hash:", e);
|
||||
}
|
||||
} else if (defaultPath) {
|
||||
setPath(defaultPath);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("hashchange", handleHashChange);
|
||||
return () => window.removeEventListener("hashchange", handleHashChange);
|
||||
}, [defaultPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
defaultPath &&
|
||||
JSON.stringify(path) !== JSON.stringify(defaultPath)
|
||||
) {
|
||||
updatePath(defaultPath);
|
||||
}
|
||||
}, [defaultPath, path, updatePath]);
|
||||
|
||||
const addPages = useCallback(
|
||||
(newPages: PageInfo[]) => {
|
||||
updatePath([...path, ...newPages]);
|
||||
},
|
||||
[path, updatePath],
|
||||
);
|
||||
|
||||
const goToIndex = useCallback(
|
||||
(index: number) => {
|
||||
updatePath(path.slice(0, index + 1));
|
||||
},
|
||||
[path, updatePath],
|
||||
);
|
||||
|
||||
const setPage = useCallback(
|
||||
(coId: CoID<RawCoValue>) => {
|
||||
updatePath([{ coId, name: "Root" }]);
|
||||
},
|
||||
[updatePath],
|
||||
);
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
if (path.length > 1) {
|
||||
updatePath(path.slice(0, path.length - 1));
|
||||
}
|
||||
}, [path, updatePath]);
|
||||
|
||||
return {
|
||||
path,
|
||||
setPage,
|
||||
addPages,
|
||||
goToIndex,
|
||||
goBack,
|
||||
};
|
||||
}
|
||||
|
||||
function encodePathToHash(path: PageInfo[]): string {
|
||||
return path
|
||||
.map((page) => {
|
||||
if (page.name && page.name !== "Root") {
|
||||
return `${page.coId}:${encodeURIComponent(page.name)}`;
|
||||
}
|
||||
return page.coId;
|
||||
})
|
||||
.join("/");
|
||||
}
|
||||
|
||||
function decodePathFromHash(hash: string): PageInfo[] {
|
||||
return hash.split("/").map((segment) => {
|
||||
const [coId, encodedName] = segment.split(":");
|
||||
return {
|
||||
coId,
|
||||
name: encodedName ? decodeURIComponent(encodedName) : undefined,
|
||||
} as PageInfo;
|
||||
});
|
||||
}
|
||||
152
examples/inspector/src/viewer/use-resolve-covalue.ts
Normal file
152
examples/inspector/src/viewer/use-resolve-covalue.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { CoID, LocalNode, RawBinaryCoStream, RawCoValue } from "cojson";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export type CoJsonType = "comap" | "costream" | "colist";
|
||||
export type ExtendedCoJsonType = "image" | "record" | "account" | "group";
|
||||
|
||||
type JSON = string | number | boolean | null | JSON[] | { [key: string]: JSON };
|
||||
type JSONObject = { [key: string]: JSON };
|
||||
|
||||
type ResolvedImageDefinition = {
|
||||
originalSize: [number, number];
|
||||
placeholderDataURL?: string;
|
||||
[res: `${number}x${number}`]: RawBinaryCoStream["id"];
|
||||
};
|
||||
|
||||
// Type guard for browser image
|
||||
export const isBrowserImage = (
|
||||
coValue: JSONObject,
|
||||
): coValue is ResolvedImageDefinition => {
|
||||
return "originalSize" in coValue && "placeholderDataURL" in coValue;
|
||||
};
|
||||
|
||||
export type ResolvedGroup = {
|
||||
readKey: string;
|
||||
[key: string]: JSON;
|
||||
};
|
||||
|
||||
export const isGroup = (coValue: JSONObject): coValue is ResolvedGroup => {
|
||||
return "readKey" in coValue;
|
||||
};
|
||||
|
||||
export type ResolvedAccount = {
|
||||
profile: {
|
||||
name: string;
|
||||
};
|
||||
[key: string]: JSON;
|
||||
};
|
||||
|
||||
export const isAccount = (coValue: JSONObject): coValue is ResolvedAccount => {
|
||||
return isGroup(coValue) && "profile" in coValue;
|
||||
};
|
||||
|
||||
export async function resolveCoValue(
|
||||
coValueId: CoID<RawCoValue>,
|
||||
node: LocalNode,
|
||||
): Promise<
|
||||
| {
|
||||
value: RawCoValue;
|
||||
snapshot: JSONObject;
|
||||
type: CoJsonType | null;
|
||||
extendedType: ExtendedCoJsonType | undefined;
|
||||
}
|
||||
| {
|
||||
value: undefined;
|
||||
snapshot: "unavailable";
|
||||
type: null;
|
||||
extendedType: undefined;
|
||||
}
|
||||
> {
|
||||
const value = await node.load(coValueId);
|
||||
|
||||
if (value === "unavailable") {
|
||||
return {
|
||||
value: undefined,
|
||||
snapshot: "unavailable",
|
||||
type: null,
|
||||
extendedType: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const snapshot = value.toJSON() as JSONObject;
|
||||
const type = value.type as CoJsonType;
|
||||
|
||||
// Determine extended type
|
||||
let extendedType: ExtendedCoJsonType | undefined;
|
||||
|
||||
if (type === "comap") {
|
||||
if (isBrowserImage(snapshot)) {
|
||||
extendedType = "image";
|
||||
} else if (isAccount(snapshot)) {
|
||||
extendedType = "account";
|
||||
} else if (isGroup(snapshot)) {
|
||||
extendedType = "group";
|
||||
} else {
|
||||
// This check is a bit of a hack
|
||||
// There might be a better way to do this
|
||||
const children = Object.values(snapshot).slice(0, 10);
|
||||
if (
|
||||
children.every(
|
||||
(c) => typeof c === "string" && c.startsWith("co_"),
|
||||
) &&
|
||||
children.length > 3
|
||||
) {
|
||||
extendedType = "record";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
value,
|
||||
snapshot,
|
||||
type,
|
||||
extendedType,
|
||||
};
|
||||
}
|
||||
|
||||
export function useResolvedCoValue(
|
||||
coValueId: CoID<RawCoValue>,
|
||||
node: LocalNode,
|
||||
) {
|
||||
const [result, setResult] =
|
||||
useState<Awaited<ReturnType<typeof resolveCoValue>>>();
|
||||
|
||||
useEffect(() => {
|
||||
resolveCoValue(coValueId, node).then(setResult);
|
||||
}, [coValueId, node]);
|
||||
|
||||
return (
|
||||
result || {
|
||||
value: undefined,
|
||||
snapshot: undefined,
|
||||
type: undefined,
|
||||
extendedType: undefined,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useResolvedCoValues(
|
||||
coValueIds: CoID<RawCoValue>[],
|
||||
node: LocalNode,
|
||||
) {
|
||||
const [results, setResults] = useState<
|
||||
Awaited<ReturnType<typeof resolveCoValue>>[]
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("RETECHING", coValueIds);
|
||||
const fetchResults = async () => {
|
||||
if (coValueIds.length === 0) return;
|
||||
const resolvedValues = await Promise.all(
|
||||
coValueIds.map((coValueId) => resolveCoValue(coValueId, node)),
|
||||
);
|
||||
|
||||
console.log({ resolvedValues });
|
||||
setResults(resolvedValues);
|
||||
};
|
||||
|
||||
fetchResults();
|
||||
}, [coValueIds, node]);
|
||||
|
||||
return results;
|
||||
}
|
||||
248
examples/inspector/src/viewer/value-renderer.tsx
Normal file
248
examples/inspector/src/viewer/value-renderer.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import clsx from "clsx";
|
||||
import { CoID, JsonValue, LocalNode, RawCoValue } from "cojson";
|
||||
import { LinkIcon } from "../link-icon";
|
||||
import {
|
||||
isBrowserImage,
|
||||
resolveCoValue,
|
||||
useResolvedCoValue,
|
||||
} from "./use-resolve-covalue";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
// Is there a chance we can pass the actual CoValue here?
|
||||
export function ValueRenderer({
|
||||
json,
|
||||
compact,
|
||||
onCoIDClick,
|
||||
}: {
|
||||
json: JsonValue | undefined;
|
||||
compact?: boolean;
|
||||
onCoIDClick?: (childNode: CoID<RawCoValue>) => void;
|
||||
}) {
|
||||
if (typeof json === "undefined" || json === undefined) {
|
||||
return <span className="text-gray-400">undefined</span>;
|
||||
}
|
||||
|
||||
if (json === null) {
|
||||
return <span className="text-gray-400">null</span>;
|
||||
}
|
||||
|
||||
if (typeof json === "string" && json.startsWith("co_")) {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"inline-flex gap-1 items-center",
|
||||
onCoIDClick &&
|
||||
"text-blue-500 cursor-pointer hover:underline",
|
||||
)}
|
||||
onClick={() => {
|
||||
onCoIDClick?.(json as CoID<RawCoValue>);
|
||||
}}
|
||||
>
|
||||
{json}
|
||||
{onCoIDClick && <LinkIcon />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof json === "string") {
|
||||
return (
|
||||
<span className="text-green-900 font-mono">
|
||||
{/* <span className="select-none opacity-70">{'"'}</span> */}
|
||||
{json}
|
||||
{/* <span className="select-none opacity-70">{'"'}</span> */}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof json === "number") {
|
||||
return <span className="text-purple-500">{json}</span>;
|
||||
}
|
||||
|
||||
if (typeof json === "boolean") {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
json
|
||||
? "text-green-700 bg-green-700/5"
|
||||
: "text-amber-700 bg-amber-500/5",
|
||||
"font-mono",
|
||||
"inline-block px-1 py-0.5 rounded",
|
||||
)}
|
||||
>
|
||||
{json.toString()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(json)) {
|
||||
return (
|
||||
<span title={JSON.stringify(json)}>
|
||||
Array <span className="text-gray-500">({json.length})</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof json === "object") {
|
||||
return (
|
||||
<span
|
||||
title={JSON.stringify(json, null, 2)}
|
||||
className="inline-block max-w-64 truncate"
|
||||
>
|
||||
{compact ? (
|
||||
<span>
|
||||
Object{" "}
|
||||
<span className="text-gray-500">
|
||||
({Object.keys(json).length})
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
JSON.stringify(json, null, 2)
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{String(json)}</span>;
|
||||
}
|
||||
|
||||
export const CoMapPreview = ({
|
||||
coId,
|
||||
node,
|
||||
limit = 6,
|
||||
}: {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
limit?: number;
|
||||
}) => {
|
||||
const { value, snapshot, type, extendedType } = useResolvedCoValue(
|
||||
coId,
|
||||
node,
|
||||
);
|
||||
|
||||
if (!snapshot) {
|
||||
return (
|
||||
<div className="rounded bg-gray-100 animate-pulse whitespace-pre w-24">
|
||||
{" "}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot === "unavailable" && !value) {
|
||||
return <div className="text-gray-500">Unavailable</div>;
|
||||
}
|
||||
|
||||
if (extendedType === "image" && isBrowserImage(snapshot)) {
|
||||
return (
|
||||
<div>
|
||||
<img
|
||||
src={snapshot.placeholderDataURL}
|
||||
className="size-8 border-2 border-white drop-shadow-md my-2"
|
||||
/>
|
||||
<span className="text-gray-500 text-sm">
|
||||
{snapshot.originalSize[0]} x {snapshot.originalSize[1]}
|
||||
</span>
|
||||
|
||||
{/* <CoMapPreview coId={value[]} node={node} /> */}
|
||||
{/* <ProgressiveImg image={value}>
|
||||
{({ src }) => <img src={src} className={clsx("w-full")} />}
|
||||
</ProgressiveImg> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (extendedType === "record") {
|
||||
return (
|
||||
<div>
|
||||
Record{" "}
|
||||
<span className="text-gray-500">
|
||||
({Object.keys(snapshot).length})
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "colist") {
|
||||
return (
|
||||
<div>
|
||||
List{" "}
|
||||
<span className="text-gray-500">
|
||||
({(snapshot as unknown as []).length})
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-sm flex flex-col gap-2 items-start">
|
||||
<div className="grid grid-cols-[auto_1fr] gap-2">
|
||||
{Object.entries(snapshot)
|
||||
.slice(0, limit)
|
||||
.map(([key, value]) => (
|
||||
<React.Fragment key={key}>
|
||||
<span className="font-medium">{key}: </span>
|
||||
<span>
|
||||
<ValueRenderer json={value} />
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
{Object.entries(snapshot).length > limit && (
|
||||
<div className="text-left text-xs text-gray-500 mt-2">
|
||||
{Object.entries(snapshot).length - limit} more
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function AccountOrGroupPreview({
|
||||
coId,
|
||||
node,
|
||||
showId = false,
|
||||
onClick,
|
||||
}: {
|
||||
coId: CoID<RawCoValue>;
|
||||
node: LocalNode;
|
||||
showId?: boolean;
|
||||
onClick?: (name?: string) => void;
|
||||
}) {
|
||||
const { snapshot, extendedType } = useResolvedCoValue(coId, node);
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (extendedType === "account") {
|
||||
resolveCoValue(
|
||||
(snapshot as unknown as { profile: CoID<RawCoValue> }).profile,
|
||||
node,
|
||||
).then(({ snapshot }) => {
|
||||
if (
|
||||
typeof snapshot === "object" &&
|
||||
"name" in snapshot &&
|
||||
typeof snapshot.name === "string"
|
||||
) {
|
||||
setName(snapshot.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [snapshot, node, extendedType]);
|
||||
|
||||
if (!snapshot) return <span>Loading...</span>;
|
||||
if (extendedType !== "account" && extendedType !== "group") {
|
||||
return <span>CoID is not an account or group</span>;
|
||||
}
|
||||
|
||||
const displayName =
|
||||
extendedType === "account" ? name || "Account" : "Group";
|
||||
const displayText = showId ? `${displayName} (${coId})` : displayName;
|
||||
|
||||
const props = onClick
|
||||
? {
|
||||
onClick: () => onClick(displayName),
|
||||
className: "text-blue-500 cursor-pointer hover:underline",
|
||||
}
|
||||
: {
|
||||
className: "text-gray-500",
|
||||
};
|
||||
|
||||
return <span {...props}>{displayText}</span>;
|
||||
}
|
||||
@@ -1,5 +1,98 @@
|
||||
# jazz-example-pets
|
||||
|
||||
## 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
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.25
|
||||
- jazz-browser-media-images@0.7.25
|
||||
- jazz-react@0.7.25
|
||||
|
||||
## 0.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.24
|
||||
- jazz-browser-media-images@0.7.24
|
||||
- jazz-react@0.7.24
|
||||
|
||||
## 0.0.88
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.23
|
||||
- jazz-tools@0.7.23
|
||||
- jazz-browser-media-images@0.7.23
|
||||
|
||||
## 0.0.87
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser-media-images@0.7.22
|
||||
- jazz-react@0.7.22
|
||||
|
||||
## 0.0.86
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.21
|
||||
- jazz-browser-media-images@0.7.21
|
||||
- jazz-react@0.7.21
|
||||
|
||||
## 0.0.85
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.20
|
||||
- jazz-browser-media-images@0.7.20
|
||||
- jazz-react@0.7.20
|
||||
|
||||
## 0.0.84
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.19
|
||||
- jazz-browser-media-images@0.7.19
|
||||
- jazz-react@0.7.19
|
||||
|
||||
## 0.0.83
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-pets",
|
||||
"private": true,
|
||||
"version": "0.0.83",
|
||||
"version": "0.0.94",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,87 @@
|
||||
# jazz-example-todo
|
||||
|
||||
## 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
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.25
|
||||
- jazz-react@0.7.25
|
||||
|
||||
## 0.0.88
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.24
|
||||
- jazz-react@0.7.24
|
||||
|
||||
## 0.0.87
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-react@0.7.23
|
||||
- jazz-tools@0.7.23
|
||||
|
||||
## 0.0.86
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.7.22
|
||||
|
||||
## 0.0.85
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.21
|
||||
- jazz-react@0.7.21
|
||||
|
||||
## 0.0.84
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.20
|
||||
- jazz-react@0.7.20
|
||||
|
||||
## 0.0.83
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.19
|
||||
- jazz-react@0.7.19
|
||||
|
||||
## 0.0.82
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.82",
|
||||
"version": "0.0.93",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -11,8 +11,8 @@ import { SiGithub, SiDiscord, SiTwitter } from "@icons-pack/react-simple-icons";
|
||||
import { Nav, NavLink, Newsletter, NewsletterButton } from "@/components/nav";
|
||||
import { DocNav } from "@/components/docs/nav";
|
||||
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next"
|
||||
import { Analytics } from "@vercel/analytics/react"
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
|
||||
// If loading a variable font, you don't need to specify the font weight
|
||||
const manrope = Manrope({
|
||||
@@ -50,8 +50,8 @@ export default function RootLayout({
|
||||
"flex flex-col items-center bg-stone-50 dark:bg-stone-950 overflow-x-hidden",
|
||||
].join(" ")}
|
||||
>
|
||||
<SpeedInsights/>
|
||||
<Analytics/>
|
||||
<SpeedInsights />
|
||||
<Analytics />
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
@@ -112,7 +112,7 @@ export default function RootLayout({
|
||||
<div className="col-span-full md:col-span-1 sm:row-start-4 md:row-start-auto lg:col-span-2 md:row-span-2 md:flex-1 flex flex-row md:flex-col max-sm:mt-4 justify-between max-sm:items-start gap-2 text-sm min-w-[10rem]">
|
||||
<GcmpLogo monochrome className="w-32" />
|
||||
<p className="max-sm:text-right">
|
||||
© 2023
|
||||
© {new Date().getFullYear()}
|
||||
<br />
|
||||
Garden Computing, Inc.
|
||||
</p>
|
||||
|
||||
@@ -268,11 +268,7 @@ Jazz Mesh is currently free — 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>
|
||||
@@ -83,4 +83,4 @@ export function GcmpLogo({
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
"packageManager": "pnpm@9.1.4",
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.3",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^15.2.2",
|
||||
"prettier": "^3.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"turbo": "^1.11.2",
|
||||
@@ -25,8 +23,7 @@
|
||||
"format": "pnpm run -r format && cd homepage/homepage && pnpm run format",
|
||||
"changeset": "changeset",
|
||||
"changeset-version": "changeset version",
|
||||
"release": "pnpm changeset publish && git push --follow-tags",
|
||||
"prepare": "husky"
|
||||
"release": "pnpm changeset publish && git push --follow-tags"
|
||||
},
|
||||
"lint-staged": {},
|
||||
"version": "0.0.0"
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
# cojson-storage-indexeddb
|
||||
|
||||
## 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
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.23
|
||||
|
||||
## 0.7.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"name": "cojson-storage-indexeddb",
|
||||
"version": "0.7.18",
|
||||
"version": "0.7.29",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:*",
|
||||
"effect": "^3.5.2",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
OutgoingSyncQueue,
|
||||
} from "cojson";
|
||||
import { SyncPromise } from "./syncPromises.js";
|
||||
import { Effect, Queue, Stream } from "effect";
|
||||
|
||||
type CoValueRow = {
|
||||
id: CojsonInternalTypes.RawCoID;
|
||||
@@ -53,11 +52,15 @@ export class IDBStorage {
|
||||
this.db = db;
|
||||
this.toLocalNode = toLocalNode;
|
||||
|
||||
void fromLocalNode.pipe(
|
||||
Stream.runForEach((msg) =>
|
||||
Effect.tryPromise({
|
||||
try: () => this.handleSyncMessage(msg),
|
||||
catch: (e) =>
|
||||
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(
|
||||
new Error(
|
||||
`Error reading from localNode, handling msg\n\n${JSON.stringify(
|
||||
msg,
|
||||
@@ -68,9 +71,13 @@ export class IDBStorage {
|
||||
)}`,
|
||||
{ cause: e },
|
||||
),
|
||||
}),
|
||||
),
|
||||
Effect.runPromise,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
processMessages().catch((e) =>
|
||||
console.error("Error in processMessages in IndexedDB", e),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,25 +89,18 @@ export class IDBStorage {
|
||||
localNodeName: "local",
|
||||
},
|
||||
): Promise<Peer> {
|
||||
return Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const [localNodeAsPeer, storageAsPeer] =
|
||||
yield* cojsonInternals.connectedPeers(
|
||||
localNodeName,
|
||||
"storage",
|
||||
{ peer1role: "client", peer2role: "server", trace },
|
||||
);
|
||||
|
||||
yield* Effect.promise(() =>
|
||||
IDBStorage.open(
|
||||
localNodeAsPeer.incoming,
|
||||
localNodeAsPeer.outgoing,
|
||||
),
|
||||
);
|
||||
|
||||
return { ...storageAsPeer, priority: 100 };
|
||||
}),
|
||||
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
|
||||
localNodeName,
|
||||
"storage",
|
||||
{ peer1role: "client", peer2role: "server", trace },
|
||||
);
|
||||
|
||||
await IDBStorage.open(
|
||||
localNodeAsPeer.incoming,
|
||||
localNodeAsPeer.outgoing,
|
||||
);
|
||||
|
||||
return { ...storageAsPeer, priority: 100 };
|
||||
}
|
||||
|
||||
static async open(
|
||||
@@ -392,35 +392,40 @@ export class IDBStorage {
|
||||
),
|
||||
).then(() => {
|
||||
// we're done with IndexedDB stuff here so can use native Promises again
|
||||
setTimeout(() =>
|
||||
Effect.runPromise(
|
||||
Effect.gen(this, function* () {
|
||||
yield* Queue.offer(this.toLocalNode, {
|
||||
action: "known",
|
||||
...ourKnown,
|
||||
asDependencyOf,
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.toLocalNode
|
||||
.push({
|
||||
action: "known",
|
||||
...ourKnown,
|
||||
asDependencyOf,
|
||||
})
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
"Error sending known state",
|
||||
e,
|
||||
),
|
||||
);
|
||||
|
||||
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) {
|
||||
yield* Queue.offer(
|
||||
this.toLocalNode,
|
||||
piece,
|
||||
);
|
||||
yield* Effect.yieldNow();
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
for (const piece of nonEmptyNewContentPieces) {
|
||||
this.toLocalNode
|
||||
.push(piece)
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
"Error sending new content piece",
|
||||
e,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
@@ -445,16 +450,18 @@ export class IDBStorage {
|
||||
const header = msg.header;
|
||||
if (!header) {
|
||||
console.error("Expected to be sent header first");
|
||||
void Effect.runPromise(
|
||||
Queue.offer(this.toLocalNode, {
|
||||
this.toLocalNode
|
||||
.push({
|
||||
action: "known",
|
||||
id: msg.id,
|
||||
header: false,
|
||||
sessions: {},
|
||||
isCorrection: true,
|
||||
}),
|
||||
);
|
||||
throw new Error("Expected to be sent header first");
|
||||
})
|
||||
.catch((e) =>
|
||||
console.error("Error sending known state", e),
|
||||
);
|
||||
return SyncPromise.resolve();
|
||||
}
|
||||
|
||||
return this.makeRequest<IDBValidKey>(({ coValues }) =>
|
||||
@@ -515,13 +522,18 @@ export class IDBStorage {
|
||||
),
|
||||
).then(() => {
|
||||
if (invalidAssumptions) {
|
||||
void Effect.runPromise(
|
||||
Queue.offer(this.toLocalNode, {
|
||||
this.toLocalNode
|
||||
.push({
|
||||
action: "known",
|
||||
...ourKnown,
|
||||
isCorrection: invalidAssumptions,
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
"Error sending known state",
|
||||
e,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
# cojson-storage-sqlite
|
||||
|
||||
## 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
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.23
|
||||
|
||||
## 0.7.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "cojson-storage-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.7.18",
|
||||
"version": "0.7.29",
|
||||
"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": {
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from "cojson";
|
||||
|
||||
import Database, { Database as DatabaseT } from "better-sqlite3";
|
||||
import { Effect, Queue, Stream } from "effect";
|
||||
|
||||
type CoValueRow = {
|
||||
id: CojsonInternalTypes.RawCoID;
|
||||
@@ -54,11 +53,15 @@ export class SQLiteStorage {
|
||||
this.db = db;
|
||||
this.toLocalNode = toLocalNode;
|
||||
|
||||
void fromLocalNode.pipe(
|
||||
Stream.runForEach((msg) =>
|
||||
Effect.tryPromise({
|
||||
try: () => this.handleSyncMessage(msg),
|
||||
catch: (e) =>
|
||||
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(
|
||||
new Error(
|
||||
`Error reading from localNode, handling msg\n\n${JSON.stringify(
|
||||
msg,
|
||||
@@ -69,9 +72,13 @@ export class SQLiteStorage {
|
||||
)}`,
|
||||
{ cause: e },
|
||||
),
|
||||
}),
|
||||
),
|
||||
Effect.runPromise,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
processMessages().catch((e) =>
|
||||
console.error("Error in processMessages in sqlite", e),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,26 +91,19 @@ export class SQLiteStorage {
|
||||
trace?: boolean;
|
||||
localNodeName?: string;
|
||||
}): Promise<Peer> {
|
||||
return Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const [localNodeAsPeer, storageAsPeer] =
|
||||
yield* cojsonInternals.connectedPeers(
|
||||
localNodeName,
|
||||
"storage",
|
||||
{ peer1role: "client", peer2role: "server", trace },
|
||||
);
|
||||
|
||||
yield* Effect.promise(() =>
|
||||
SQLiteStorage.open(
|
||||
filename,
|
||||
localNodeAsPeer.incoming,
|
||||
localNodeAsPeer.outgoing,
|
||||
),
|
||||
);
|
||||
|
||||
return { ...storageAsPeer, priority: 100 };
|
||||
}),
|
||||
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
|
||||
localNodeName,
|
||||
"storage",
|
||||
{ peer1role: "client", peer2role: "server", trace },
|
||||
);
|
||||
|
||||
await SQLiteStorage.open(
|
||||
filename,
|
||||
localNodeAsPeer.incoming,
|
||||
localNodeAsPeer.outgoing,
|
||||
);
|
||||
|
||||
return { ...storageAsPeer, priority: 100 };
|
||||
}
|
||||
|
||||
static async open(
|
||||
@@ -441,13 +441,13 @@ export class SQLiteStorage {
|
||||
);
|
||||
}
|
||||
|
||||
await Effect.runPromise(
|
||||
Queue.offer(this.toLocalNode, {
|
||||
this.toLocalNode
|
||||
.push({
|
||||
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,7 +456,11 @@ export class SQLiteStorage {
|
||||
// console.log(theirKnown.id, nonEmptyNewContentPieces);
|
||||
|
||||
for (const piece of nonEmptyNewContentPieces) {
|
||||
await Effect.runPromise(Queue.offer(this.toLocalNode, piece));
|
||||
this.toLocalNode
|
||||
.push(piece)
|
||||
.catch((e) =>
|
||||
console.error("Error while pushing content piece", e),
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
@@ -478,15 +482,17 @@ export class SQLiteStorage {
|
||||
const header = msg.header;
|
||||
if (!header) {
|
||||
console.error("Expected to be sent header first");
|
||||
await Effect.runPromise(
|
||||
Queue.offer(this.toLocalNode, {
|
||||
this.toLocalNode
|
||||
.push({
|
||||
action: "known",
|
||||
id: msg.id,
|
||||
header: false,
|
||||
sessions: {},
|
||||
isCorrection: true,
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch((e) =>
|
||||
console.error("Error while pushing known", e),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -618,13 +624,13 @@ export class SQLiteStorage {
|
||||
})();
|
||||
|
||||
if (invalidAssumptions) {
|
||||
await Effect.runPromise(
|
||||
Queue.offer(this.toLocalNode, {
|
||||
this.toLocalNode
|
||||
.push({
|
||||
action: "known",
|
||||
...ourKnown,
|
||||
isCorrection: invalidAssumptions,
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch((e) => console.error("Error while pushing known", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,46 @@
|
||||
# cojson-transport-nodejs-ws
|
||||
|
||||
## 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
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.23
|
||||
|
||||
## 0.7.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Increase disconnect timeout for now
|
||||
|
||||
## 0.7.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"name": "cojson-transport-ws",
|
||||
"type": "module",
|
||||
"version": "0.7.18",
|
||||
"version": "0.7.29",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:*",
|
||||
"effect": "^3.5.2",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,108 +1,118 @@
|
||||
import { DisconnectedError, Peer, PingTimeoutError, SyncMessage } from "cojson";
|
||||
import { Either, Stream, Queue, Effect, Exit } from "effect";
|
||||
import {
|
||||
DisconnectedError,
|
||||
Peer,
|
||||
PingTimeoutError,
|
||||
SyncMessage,
|
||||
cojsonInternals,
|
||||
} from "cojson";
|
||||
|
||||
interface AnyWebSocket {
|
||||
addEventListener(
|
||||
type: "close",
|
||||
listener: (event: { code: number; reason: string }) => void,
|
||||
): void;
|
||||
addEventListener(
|
||||
type: "message",
|
||||
listener: (event: { data: string | unknown }) => void,
|
||||
): void;
|
||||
addEventListener(type: "open", listener: () => void): void;
|
||||
close(): void;
|
||||
send(data: string): void;
|
||||
interface WebsocketEvents {
|
||||
close: { code: number; reason: string };
|
||||
message: { data: unknown };
|
||||
open: void;
|
||||
}
|
||||
interface PingMsg {
|
||||
time: number;
|
||||
dc: string;
|
||||
}
|
||||
|
||||
export function createWebSocketPeer(options: {
|
||||
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,
|
||||
listener: (event: WebsocketEvents[K]) => void,
|
||||
): void;
|
||||
close(): void;
|
||||
send(data: string): void;
|
||||
readyState: number;
|
||||
}
|
||||
|
||||
const g: typeof globalThis & {
|
||||
jazzPings?: {
|
||||
received: number;
|
||||
sent: number;
|
||||
dc: string;
|
||||
}[];
|
||||
} = globalThis;
|
||||
|
||||
export function createWebSocketPeer({
|
||||
id,
|
||||
websocket,
|
||||
role,
|
||||
expectPings = true,
|
||||
}: {
|
||||
id: string;
|
||||
websocket: AnyWebSocket;
|
||||
role: Peer["role"];
|
||||
}): Effect.Effect<Peer> {
|
||||
return Effect.gen(function* () {
|
||||
const ws = options.websocket;
|
||||
expectPings?: boolean;
|
||||
}): Peer {
|
||||
const incoming = new cojsonInternals.Channel<
|
||||
SyncMessage | DisconnectedError | PingTimeoutError
|
||||
>();
|
||||
|
||||
const incoming =
|
||||
yield* Queue.unbounded<
|
||||
Either.Either<SyncMessage, DisconnectedError | PingTimeoutError>
|
||||
>();
|
||||
const outgoing = yield* Queue.unbounded<SyncMessage>();
|
||||
|
||||
ws.addEventListener("close", (event) => {
|
||||
void Effect.runPromiseExit(
|
||||
Queue.offer(
|
||||
incoming,
|
||||
Either.left(
|
||||
new DisconnectedError(`${event.code}: ${event.reason}`),
|
||||
),
|
||||
),
|
||||
).then((e) => {
|
||||
if (Exit.isFailure(e) && !Exit.isInterrupted(e)) {
|
||||
console.warn("Failed closing ws", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let pingTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
const msg = JSON.parse(event.data as string);
|
||||
|
||||
if (pingTimeout) {
|
||||
clearTimeout(pingTimeout);
|
||||
}
|
||||
|
||||
pingTimeout = setTimeout(() => {
|
||||
console.debug("Ping timeout");
|
||||
void Effect.runPromise(
|
||||
Queue.offer(incoming, Either.left(new PingTimeoutError())),
|
||||
);
|
||||
try {
|
||||
ws.close();
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Error while trying to close ws on ping timeout",
|
||||
e,
|
||||
);
|
||||
}
|
||||
}, 2500);
|
||||
|
||||
if (msg.type === "ping") {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).jazzPings =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).jazzPings || [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).jazzPings.push({
|
||||
received: Date.now(),
|
||||
sent: msg.time,
|
||||
dc: msg.dc,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
void Effect.runPromise(
|
||||
Queue.offer(incoming, Either.right(msg)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
void Stream.fromQueue(outgoing).pipe(
|
||||
Stream.runForEach((msg) =>
|
||||
Effect.sync(() => ws.send(JSON.stringify(msg))),
|
||||
),
|
||||
Effect.runPromise,
|
||||
websocket.addEventListener("close", function handleClose() {
|
||||
incoming
|
||||
.push("Disconnected")
|
||||
.catch((e) =>
|
||||
console.error("Error while pushing disconnect msg", e),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
id: options.id,
|
||||
incoming: Stream.fromQueue(incoming, { shutdown: true }).pipe(
|
||||
Stream.mapEffect((either) => either),
|
||||
),
|
||||
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) => {
|
||||
websocket.addEventListener("open", resolve, { once: true });
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
incoming,
|
||||
outgoing: {
|
||||
async push(msg) {
|
||||
await websocketOpen;
|
||||
if (websocket.readyState === 1) {
|
||||
websocket.send(JSON.stringify(msg));
|
||||
}
|
||||
},
|
||||
close() {
|
||||
if (websocket.readyState === 1) {
|
||||
websocket.close();
|
||||
}
|
||||
},
|
||||
},
|
||||
role,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
# cojson
|
||||
|
||||
## 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
|
||||
|
||||
- Mostly complete OPFS implementation (single-tab only)
|
||||
|
||||
## 0.7.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"types": "src/index.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.7.18",
|
||||
"version": "0.7.29",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
@@ -18,12 +18,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hazae41/berith": "^1.2.6",
|
||||
"@noble/curves": "^1.3.0",
|
||||
"@noble/ciphers": "^0.1.3",
|
||||
"@noble/curves": "^1.3.0",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@scure/base": "^1.1.1",
|
||||
"effect": "^3.5.2",
|
||||
"hash-wasm": "^4.9.0"
|
||||
"hash-wasm": "^4.9.0",
|
||||
"queueable": "^5.3.2"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsc --watch --sourceMap --outDir dist",
|
||||
|
||||
@@ -26,6 +26,13 @@ import { expectGroup } from "./typeUtils/expectGroup.js";
|
||||
import { isAccountID } from "./typeUtils/isAccountID.js";
|
||||
import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
|
||||
|
||||
/**
|
||||
In order to not block other concurrently syncing CoValues we introduce a maximum size of transactions,
|
||||
since they are the smallest unit of progress that can be synced within a CoValue.
|
||||
This is particularly important for storing binary data in CoValues, since they are likely to be at least on the order of megabytes.
|
||||
This also means that we want to keep signatures roughly after each MAX_RECOMMENDED_TX size chunk,
|
||||
to be able to verify partially loaded CoValues or CoValues that are still being created (like a video live stream).
|
||||
**/
|
||||
export const MAX_RECOMMENDED_TX_SIZE = 100 * 1024;
|
||||
|
||||
export type CoValueHeader = {
|
||||
@@ -383,7 +390,7 @@ export class CoValueCore {
|
||||
0,
|
||||
);
|
||||
|
||||
if (sizeOfTxsSinceLastInbetweenSignature > 100 * 1024) {
|
||||
if (sizeOfTxsSinceLastInbetweenSignature > MAX_RECOMMENDED_TX_SIZE) {
|
||||
// console.log(
|
||||
// "Saving inbetween signature for tx ",
|
||||
// sessionID,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "./crypto/crypto.js";
|
||||
import { WasmCrypto } from "./crypto/WasmCrypto.js";
|
||||
import { PureJSCrypto } from "./crypto/PureJSCrypto.js";
|
||||
import { connectedPeers } from "./streamUtils.js";
|
||||
import { connectedPeers, Channel } from "./streamUtils.js";
|
||||
import { ControlledAgent, RawControlledAccount } from "./coValues/account.js";
|
||||
import type { Role } from "./permissions.js";
|
||||
import { rawCoIDtoBytes, rawCoIDfromBytes, isRawCoID } from "./ids.js";
|
||||
@@ -59,12 +59,7 @@ import type * as Media from "./media.js";
|
||||
|
||||
type Value = JsonValue | AnyRawCoValue;
|
||||
|
||||
import {
|
||||
LSMStorage,
|
||||
FSErr,
|
||||
BlockFilename,
|
||||
WalFilename,
|
||||
} from "./storage/index.js";
|
||||
import { LSMStorage, BlockFilename, WalFilename } from "./storage/index.js";
|
||||
import { FileSystem } from "./storage/FileSystem.js";
|
||||
|
||||
/** @hidden */
|
||||
@@ -84,6 +79,7 @@ export const cojsonInternals = {
|
||||
accountHeaderForInitialAgentSecret,
|
||||
idforHeader,
|
||||
StreamingHash,
|
||||
Channel,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -123,18 +119,17 @@ 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
|
||||
|
||||
@@ -670,6 +670,10 @@ export class LocalNode {
|
||||
|
||||
return newNode;
|
||||
}
|
||||
|
||||
gracefulShutdown() {
|
||||
this.syncManager.gracefulShutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Effect } from "effect";
|
||||
import { CoValueChunk } from "./index.js";
|
||||
import { RawCoID } from "../ids.js";
|
||||
import { CryptoProvider, StreamingHash } from "../crypto/crypto.js";
|
||||
|
||||
export type BlockFilename = `${string}-L${number}-H${number}.jsonl`;
|
||||
export type BlockFilename = `L${number}-${string}-${string}-H${number}.jsonl`;
|
||||
|
||||
export type BlockHeader = { id: RawCoID; start: number; length: number }[];
|
||||
|
||||
@@ -11,143 +10,124 @@ 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): 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>;
|
||||
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 }>;
|
||||
read(
|
||||
handle: ReadHandle,
|
||||
offset: number,
|
||||
length: number,
|
||||
): Effect.Effect<Uint8Array, FSErr>;
|
||||
listFiles(): Effect.Effect<string[], FSErr>;
|
||||
removeFile(
|
||||
filename: BlockFilename | WalFilename,
|
||||
): Effect.Effect<void, FSErr>;
|
||||
): Promise<Uint8Array>;
|
||||
listFiles(): Promise<string[]>;
|
||||
removeFile(filename: BlockFilename | WalFilename): Promise<void>;
|
||||
}
|
||||
|
||||
export const textEncoder = new TextEncoder();
|
||||
export const textDecoder = new TextDecoder();
|
||||
|
||||
export function readChunk<RH, FS extends FileSystem<unknown, RH>>(
|
||||
export async function readChunk<RH, FS extends FileSystem<unknown, RH>>(
|
||||
handle: RH,
|
||||
header: { start: number; length: number },
|
||||
fs: FS,
|
||||
): Effect.Effect<CoValueChunk, FSErr> {
|
||||
return Effect.gen(function* ($) {
|
||||
const chunkBytes = yield* $(
|
||||
fs.read(handle, header.start, header.length),
|
||||
);
|
||||
): Promise<CoValueChunk> {
|
||||
const chunkBytes = await 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 function readHeader<RH, FS extends FileSystem<unknown, RH>>(
|
||||
export async function readHeader<RH, FS extends FileSystem<unknown, RH>>(
|
||||
filename: string,
|
||||
handle: RH,
|
||||
size: number,
|
||||
fs: FS,
|
||||
): Effect.Effect<BlockHeader, FSErr> {
|
||||
return Effect.gen(function* ($) {
|
||||
const headerLength = Number(filename.match(/-H(\d+)\.jsonl$/)![1]!);
|
||||
): Promise<BlockHeader> {
|
||||
const headerLength = Number(filename.match(/-H(\d+)\.jsonl$/)![1]!);
|
||||
|
||||
const headerBytes = yield* $(
|
||||
fs.read(handle, size - headerLength, headerLength),
|
||||
);
|
||||
const headerBytes = await 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 function writeBlock<WH, RH, FS extends FileSystem<WH, RH>>(
|
||||
export async function writeBlock<WH, RH, FS extends FileSystem<WH, RH>>(
|
||||
chunks: Map<RawCoID, CoValueChunk>,
|
||||
level: number,
|
||||
blockNumber: number,
|
||||
fs: FS,
|
||||
): Effect.Effect<void, FSErr> {
|
||||
): Promise<BlockFilename> {
|
||||
if (chunks.size === 0) {
|
||||
return Effect.die(new Error("No chunks to write"));
|
||||
throw new Error("No chunks to write");
|
||||
}
|
||||
|
||||
return Effect.gen(function* ($) {
|
||||
const blockHeader: BlockHeader = [];
|
||||
const blockHeader: BlockHeader = [];
|
||||
|
||||
let offset = 0;
|
||||
let offset = 0;
|
||||
|
||||
const file = yield* $(
|
||||
fs.createFile(
|
||||
"wipBlock" +
|
||||
Math.random().toString(36).substring(7) +
|
||||
".tmp.jsonl",
|
||||
),
|
||||
);
|
||||
const hash = new StreamingHash(fs.crypto);
|
||||
const file = await 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;
|
||||
yield* $(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;
|
||||
await fs.append(file, encodedBytesWithNewline);
|
||||
const length = encodedBytesWithNewline.length;
|
||||
blockHeader.push({ id, start: offset, length });
|
||||
offset += length;
|
||||
}
|
||||
|
||||
const headerBytes = textEncoder.encode(JSON.stringify(blockHeader));
|
||||
yield* $(fs.append(file, headerBytes));
|
||||
const headerBytes = textEncoder.encode(JSON.stringify(blockHeader));
|
||||
await 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 = `${hash.digest()}-L${level}-H${
|
||||
headerBytes.length
|
||||
}.jsonl`;
|
||||
// console.log("renaming to" + filename);
|
||||
yield* $(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);
|
||||
await fs.closeAndRename(file, filename);
|
||||
|
||||
// console.log("Wrote block", filename, blockHeader);
|
||||
// console.log("IDs in block", blockHeader.map(e => e.id));
|
||||
});
|
||||
return filename;
|
||||
|
||||
// console.log("Wrote block", filename, blockHeader);
|
||||
// console.log("IDs in block", blockHeader.map(e => e.id));
|
||||
}
|
||||
|
||||
export function writeToWal<WH, RH, FS extends FileSystem<WH, RH>>(
|
||||
export async function writeToWal<WH, RH, FS extends FileSystem<WH, RH>>(
|
||||
handle: WH,
|
||||
fs: FS,
|
||||
id: RawCoID,
|
||||
chunk: CoValueChunk,
|
||||
): Effect.Effect<void, FSErr> {
|
||||
return Effect.gen(function* ($) {
|
||||
const walEntry: WalEntry = {
|
||||
id,
|
||||
...chunk,
|
||||
};
|
||||
const bytes = textEncoder.encode(JSON.stringify(walEntry) + "\n");
|
||||
yield* $(fs.append(handle, bytes));
|
||||
});
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Either } from "effect";
|
||||
import { RawCoID, SessionID } from "../ids.js";
|
||||
import { MAX_RECOMMENDED_TX_SIZE } from "../index.js";
|
||||
import { CoValueKnownState, NewContentMessage } from "../sync.js";
|
||||
@@ -80,7 +79,7 @@ export function chunkToKnownState(id: RawCoID, chunk: CoValueChunk) {
|
||||
export function mergeChunks(
|
||||
chunkA: CoValueChunk,
|
||||
chunkB: CoValueChunk,
|
||||
): Either.Either<"nonContigous", CoValueChunk> {
|
||||
): "nonContigous" | CoValueChunk {
|
||||
const header = chunkA.header || chunkB.header;
|
||||
|
||||
const newSessions = { ...chunkA.sessionEntries };
|
||||
@@ -133,9 +132,9 @@ export function mergeChunks(
|
||||
}
|
||||
newSessions[sessionID] = newEntries;
|
||||
} else {
|
||||
return Either.right("nonContigous" as const);
|
||||
return "nonContigous" as const;
|
||||
}
|
||||
}
|
||||
|
||||
return Either.left({ header, sessionEntries: newSessions });
|
||||
return { header, sessionEntries: newSessions };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Effect, Either, Queue, Stream, SynchronizedRef } from "effect";
|
||||
import { RawCoID } from "../ids.js";
|
||||
import { CoValueHeader, Transaction } from "../coValueCore.js";
|
||||
import { Signature } from "../crypto/crypto.js";
|
||||
@@ -18,7 +17,6 @@ import {
|
||||
} from "./chunksAndKnownStates.js";
|
||||
import {
|
||||
BlockFilename,
|
||||
FSErr,
|
||||
FileSystem,
|
||||
WalEntry,
|
||||
WalFilename,
|
||||
@@ -28,7 +26,9 @@ import {
|
||||
writeBlock,
|
||||
writeToWal,
|
||||
} from "./FileSystem.js";
|
||||
export type { FSErr, BlockFilename, WalFilename } from "./FileSystem.js";
|
||||
export type { BlockFilename, WalFilename } from "./FileSystem.js";
|
||||
|
||||
const MAX_N_LEVELS = 3;
|
||||
|
||||
export type CoValueChunk = {
|
||||
header?: CoValueHeader;
|
||||
@@ -42,410 +42,511 @@ export type CoValueChunk = {
|
||||
};
|
||||
|
||||
export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
|
||||
currentWal: SynchronizedRef.SynchronizedRef<WH | undefined>;
|
||||
coValues: SynchronizedRef.SynchronizedRef<{
|
||||
currentWal: WH | undefined;
|
||||
coValues: {
|
||||
[id: RawCoID]: CoValueChunk | undefined;
|
||||
}>;
|
||||
};
|
||||
fileCache: string[] | undefined;
|
||||
headerCache = new Map<
|
||||
BlockFilename,
|
||||
{ [id: RawCoID]: { start: number; length: number } }
|
||||
>();
|
||||
blockFileHandles = new Map<
|
||||
BlockFilename,
|
||||
Promise<{ handle: RH; size: number }>
|
||||
>();
|
||||
|
||||
constructor(
|
||||
public fs: FS,
|
||||
public fromLocalNode: IncomingSyncStream,
|
||||
public toLocalNode: OutgoingSyncQueue,
|
||||
) {
|
||||
this.coValues = SynchronizedRef.unsafeMake({});
|
||||
this.currentWal = SynchronizedRef.unsafeMake<WH | undefined>(undefined);
|
||||
this.coValues = {};
|
||||
this.currentWal = undefined;
|
||||
|
||||
void this.fromLocalNode.pipe(
|
||||
Stream.runForEach((msg) =>
|
||||
Effect.gen(this, function* () {
|
||||
let nMsg = 0;
|
||||
|
||||
const processMessages = async () => {
|
||||
for await (const msg of fromLocalNode) {
|
||||
console.log("Storage msg start", nMsg);
|
||||
try {
|
||||
if (msg === "Disconnected" || msg === "PingTimeout") {
|
||||
throw new Error("Unexpected Disconnected message");
|
||||
}
|
||||
if (msg.action === "done") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.action === "content") {
|
||||
yield* this.handleNewContent(msg);
|
||||
await this.handleNewContent(msg);
|
||||
} else {
|
||||
yield* this.sendNewContent(msg.id, msg, undefined);
|
||||
await this.sendNewContent(msg.id, msg, undefined);
|
||||
}
|
||||
}),
|
||||
),
|
||||
Effect.runPromise,
|
||||
);
|
||||
|
||||
setTimeout(() => this.compact(), 20000);
|
||||
}
|
||||
|
||||
sendNewContent(
|
||||
id: RawCoID,
|
||||
known: CoValueKnownState | undefined,
|
||||
asDependencyOf: RawCoID | undefined,
|
||||
): Effect.Effect<void, FSErr> {
|
||||
return SynchronizedRef.updateEffect(this.coValues, (coValues) =>
|
||||
this.sendNewContentInner(coValues, id, known, asDependencyOf),
|
||||
);
|
||||
}
|
||||
|
||||
private sendNewContentInner(
|
||||
coValues: { [id: `co_z${string}`]: CoValueChunk | undefined },
|
||||
id: RawCoID,
|
||||
known: CoValueKnownState | undefined,
|
||||
asDependencyOf: RawCoID | undefined,
|
||||
): Effect.Effect<
|
||||
{ [id: `co_z${string}`]: CoValueChunk | undefined },
|
||||
FSErr,
|
||||
never
|
||||
> {
|
||||
return Effect.gen(this, function* () {
|
||||
let coValue = coValues[id];
|
||||
|
||||
if (!coValue) {
|
||||
coValue = yield* this.loadCoValue(id, this.fs);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
new Error(
|
||||
`Error reading from localNode, handling msg\n\n${JSON.stringify(
|
||||
msg,
|
||||
(k, v) =>
|
||||
k === "changes" || k === "encryptedChanges"
|
||||
? v.slice(0, 20) + "..."
|
||||
: v,
|
||||
)}`,
|
||||
{ cause: e },
|
||||
),
|
||||
);
|
||||
}
|
||||
console.log("Storage msg end", nMsg);
|
||||
nMsg++;
|
||||
}
|
||||
};
|
||||
|
||||
if (!coValue) {
|
||||
yield* Queue.offer(this.toLocalNode, {
|
||||
processMessages().catch((e) =>
|
||||
console.error("Error in processMessages in storage", e),
|
||||
);
|
||||
|
||||
setTimeout(
|
||||
() =>
|
||||
this.compact().catch((e) => {
|
||||
console.error("Error while compacting", e);
|
||||
}),
|
||||
20000,
|
||||
);
|
||||
}
|
||||
|
||||
async sendNewContent(
|
||||
id: RawCoID,
|
||||
known: CoValueKnownState | undefined,
|
||||
asDependencyOf: RawCoID | undefined,
|
||||
) {
|
||||
let coValue = this.coValues[id];
|
||||
|
||||
if (!coValue) {
|
||||
coValue = await this.loadCoValue(id, this.fs);
|
||||
}
|
||||
|
||||
if (!coValue) {
|
||||
this.toLocalNode
|
||||
.push({
|
||||
id: id,
|
||||
action: "known",
|
||||
header: false,
|
||||
sessions: {},
|
||||
asDependencyOf,
|
||||
});
|
||||
})
|
||||
.catch((e) => console.error("Error while pushing known", e));
|
||||
|
||||
return coValues;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!known?.header &&
|
||||
coValue.header?.ruleset.type === "ownedByGroup"
|
||||
) {
|
||||
coValues = yield* this.sendNewContentInner(
|
||||
coValues,
|
||||
coValue.header.ruleset.group,
|
||||
undefined,
|
||||
asDependencyOf || id,
|
||||
);
|
||||
} else if (
|
||||
!known?.header &&
|
||||
coValue.header?.ruleset.type === "group"
|
||||
) {
|
||||
const dependedOnAccounts = new Set();
|
||||
for (const session of Object.values(coValue.sessionEntries)) {
|
||||
for (const entry of session) {
|
||||
for (const tx of entry.transactions) {
|
||||
if (tx.privacy === "trusting") {
|
||||
const parsedChanges = JSON.parse(tx.changes);
|
||||
for (const change of parsedChanges) {
|
||||
if (
|
||||
change.op === "set" &&
|
||||
change.key.startsWith("co_")
|
||||
) {
|
||||
dependedOnAccounts.add(change.key);
|
||||
}
|
||||
if (!known?.header && coValue.header?.ruleset.type === "ownedByGroup") {
|
||||
await this.sendNewContent(
|
||||
coValue.header.ruleset.group,
|
||||
undefined,
|
||||
asDependencyOf || id,
|
||||
);
|
||||
} else if (!known?.header && coValue.header?.ruleset.type === "group") {
|
||||
const dependedOnAccounts = new Set();
|
||||
for (const session of Object.values(coValue.sessionEntries)) {
|
||||
for (const entry of session) {
|
||||
for (const tx of entry.transactions) {
|
||||
if (tx.privacy === "trusting") {
|
||||
const parsedChanges = JSON.parse(tx.changes);
|
||||
for (const change of parsedChanges) {
|
||||
if (
|
||||
change.op === "set" &&
|
||||
change.key.startsWith("co_")
|
||||
) {
|
||||
dependedOnAccounts.add(change.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const account of dependedOnAccounts) {
|
||||
coValues = yield* this.sendNewContentInner(
|
||||
coValues,
|
||||
account as CoID<RawCoValue>,
|
||||
undefined,
|
||||
asDependencyOf || id,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const account of dependedOnAccounts) {
|
||||
await this.sendNewContent(
|
||||
account as CoID<RawCoValue>,
|
||||
undefined,
|
||||
asDependencyOf || id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const newContentMessages = contentSinceChunk(
|
||||
id,
|
||||
coValue,
|
||||
known,
|
||||
).map((message) => ({ ...message, asDependencyOf }));
|
||||
const newContentMessages = contentSinceChunk(id, coValue, known).map(
|
||||
(message) => ({ ...message, asDependencyOf }),
|
||||
);
|
||||
|
||||
const ourKnown: CoValueKnownState = chunkToKnownState(id, coValue);
|
||||
const ourKnown: CoValueKnownState = chunkToKnownState(id, coValue);
|
||||
|
||||
yield* Queue.offer(this.toLocalNode, {
|
||||
this.toLocalNode
|
||||
.push({
|
||||
action: "known",
|
||||
...ourKnown,
|
||||
asDependencyOf,
|
||||
});
|
||||
})
|
||||
.catch((e) => console.error("Error while pushing known", e));
|
||||
|
||||
for (const message of newContentMessages) {
|
||||
if (Object.keys(message.new).length === 0) continue;
|
||||
yield* Queue.offer(this.toLocalNode, message);
|
||||
for (const message of newContentMessages) {
|
||||
if (Object.keys(message.new).length === 0) continue;
|
||||
this.toLocalNode
|
||||
.push(message)
|
||||
.catch((e) =>
|
||||
console.error("Error while pushing new content", e),
|
||||
);
|
||||
}
|
||||
|
||||
this.coValues[id] = coValue;
|
||||
}
|
||||
|
||||
async withWAL(handler: (wal: WH) => Promise<void>) {
|
||||
if (!this.currentWal) {
|
||||
this.currentWal = await this.fs.createFile(
|
||||
`wal-${Date.now()}-${Math.random()
|
||||
.toString(36)
|
||||
.slice(2)}.jsonl`,
|
||||
);
|
||||
}
|
||||
await handler(this.currentWal);
|
||||
}
|
||||
|
||||
async handleNewContent(newContent: NewContentMessage) {
|
||||
const coValue = this.coValues[newContent.id];
|
||||
|
||||
const newContentAsChunk: CoValueChunk = {
|
||||
header: newContent.header,
|
||||
sessionEntries: Object.fromEntries(
|
||||
Object.entries(newContent.new).map(
|
||||
([sessionID, newInSession]) => [
|
||||
sessionID,
|
||||
[
|
||||
{
|
||||
after: newInSession.after,
|
||||
lastSignature: newInSession.lastSignature,
|
||||
transactions: newInSession.newTransactions,
|
||||
},
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
if (!coValue) {
|
||||
if (newContent.header) {
|
||||
// console.log("Creating in WAL", newContent.id);
|
||||
await this.withWAL((wal) =>
|
||||
writeToWal(wal, this.fs, newContent.id, newContentAsChunk),
|
||||
);
|
||||
|
||||
this.coValues[newContent.id] = newContentAsChunk;
|
||||
} else {
|
||||
console.warn(
|
||||
"Incontiguous incoming update for " + newContent.id,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
return { ...coValues, [id]: coValue };
|
||||
});
|
||||
}
|
||||
|
||||
withWAL(
|
||||
handler: (wal: WH) => Effect.Effect<void, FSErr>,
|
||||
): Effect.Effect<void, FSErr> {
|
||||
return SynchronizedRef.updateEffect(this.currentWal, (wal) =>
|
||||
Effect.gen(this, function* () {
|
||||
let newWal = wal;
|
||||
if (!newWal) {
|
||||
newWal = yield* this.fs.createFile(
|
||||
`wal-${new Date().toISOString()}-${Math.random()
|
||||
.toString(36)
|
||||
.slice(2)}.jsonl`,
|
||||
);
|
||||
}
|
||||
yield* handler(newWal);
|
||||
return newWal;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
handleNewContent(
|
||||
newContent: NewContentMessage,
|
||||
): Effect.Effect<void, FSErr> {
|
||||
return SynchronizedRef.updateEffect(this.coValues, (coValues) =>
|
||||
Effect.gen(this, function* () {
|
||||
const coValue = coValues[newContent.id];
|
||||
|
||||
const newContentAsChunk: CoValueChunk = {
|
||||
header: newContent.header,
|
||||
sessionEntries: Object.fromEntries(
|
||||
Object.entries(newContent.new).map(
|
||||
([sessionID, newInSession]) => [
|
||||
sessionID,
|
||||
[
|
||||
{
|
||||
after: newInSession.after,
|
||||
lastSignature:
|
||||
newInSession.lastSignature,
|
||||
transactions:
|
||||
newInSession.newTransactions,
|
||||
},
|
||||
],
|
||||
],
|
||||
),
|
||||
} else {
|
||||
const merged = mergeChunks(coValue, newContentAsChunk);
|
||||
if (merged === "nonContigous") {
|
||||
console.warn(
|
||||
"Non-contigous new content for " + newContent.id,
|
||||
Object.entries(coValue.sessionEntries).map(
|
||||
([session, entries]) =>
|
||||
entries.map((entry) => ({
|
||||
session: session,
|
||||
after: entry.after,
|
||||
length: entry.transactions.length,
|
||||
})),
|
||||
),
|
||||
};
|
||||
Object.entries(newContentAsChunk.sessionEntries).map(
|
||||
([session, entries]) =>
|
||||
entries.map((entry) => ({
|
||||
session: session,
|
||||
after: entry.after,
|
||||
length: entry.transactions.length,
|
||||
})),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// console.log("Appending to WAL", newContent.id);
|
||||
await this.withWAL((wal) =>
|
||||
writeToWal(wal, this.fs, newContent.id, newContentAsChunk),
|
||||
);
|
||||
|
||||
if (!coValue) {
|
||||
if (newContent.header) {
|
||||
// console.log("Creating in WAL", newContent.id);
|
||||
yield* this.withWAL((wal) =>
|
||||
writeToWal(
|
||||
wal,
|
||||
this.fs,
|
||||
newContent.id,
|
||||
newContentAsChunk,
|
||||
),
|
||||
this.coValues[newContent.id] = merged;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getBlockHandle(
|
||||
blockFile: BlockFilename,
|
||||
fs: FS,
|
||||
): Promise<{ handle: RH; size: number }> {
|
||||
if (!this.blockFileHandles.has(blockFile)) {
|
||||
this.blockFileHandles.set(blockFile, fs.openToRead(blockFile));
|
||||
}
|
||||
|
||||
return this.blockFileHandles.get(blockFile)!;
|
||||
}
|
||||
|
||||
async loadCoValue(id: RawCoID, fs: FS): Promise<CoValueChunk | undefined> {
|
||||
const files = this.fileCache || (await fs.listFiles());
|
||||
this.fileCache = files;
|
||||
const blockFiles = (
|
||||
files.filter((name) => name.startsWith("L")) as BlockFilename[]
|
||||
).sort();
|
||||
|
||||
let result;
|
||||
|
||||
for (const blockFile of blockFiles) {
|
||||
let cachedHeader:
|
||||
| { [id: RawCoID]: { start: number; length: number } }
|
||||
| undefined = this.headerCache.get(blockFile);
|
||||
|
||||
const { handle, size } = await this.getBlockHandle(blockFile, fs);
|
||||
|
||||
// console.log("Attempting to load", id, blockFile);
|
||||
|
||||
if (!cachedHeader) {
|
||||
cachedHeader = {};
|
||||
const header = await readHeader(blockFile, handle, size, fs);
|
||||
for (const entry of header) {
|
||||
cachedHeader[entry.id] = {
|
||||
start: entry.start,
|
||||
length: entry.length,
|
||||
};
|
||||
}
|
||||
|
||||
this.headerCache.set(blockFile, cachedHeader);
|
||||
}
|
||||
const headerEntry = cachedHeader[id];
|
||||
|
||||
// console.log("Header entry", id, headerEntry);
|
||||
|
||||
if (headerEntry) {
|
||||
const nextChunk = await readChunk(handle, headerEntry, fs);
|
||||
if (result) {
|
||||
const merged = mergeChunks(result, nextChunk);
|
||||
|
||||
if (merged === "nonContigous") {
|
||||
console.warn(
|
||||
"Non-contigous chunks while loading " + id,
|
||||
result,
|
||||
nextChunk,
|
||||
);
|
||||
|
||||
return {
|
||||
...coValues,
|
||||
[newContent.id]: newContentAsChunk,
|
||||
};
|
||||
} else {
|
||||
// yield*
|
||||
// Effect.promise(() =>
|
||||
// this.toLocalNode.write({
|
||||
// action: "known",
|
||||
// id: newContent.id,
|
||||
// header: false,
|
||||
// sessions: {},
|
||||
// isCorrection: true,
|
||||
// })
|
||||
// )
|
||||
// );
|
||||
yield* Effect.logWarning(
|
||||
"Incontiguous incoming update for " + newContent.id,
|
||||
);
|
||||
return coValues;
|
||||
result = merged;
|
||||
}
|
||||
} else {
|
||||
const merged = mergeChunks(coValue, newContentAsChunk);
|
||||
if (Either.isRight(merged)) {
|
||||
yield* Effect.logWarning(
|
||||
"Non-contigous new content for " + newContent.id,
|
||||
Object.entries(coValue.sessionEntries).map(
|
||||
([session, entries]) =>
|
||||
entries.map((entry) => ({
|
||||
session: session,
|
||||
after: entry.after,
|
||||
length: entry.transactions.length,
|
||||
})),
|
||||
),
|
||||
Object.entries(
|
||||
newContentAsChunk.sessionEntries,
|
||||
).map(([session, entries]) =>
|
||||
entries.map((entry) => ({
|
||||
session: session,
|
||||
after: entry.after,
|
||||
length: entry.transactions.length,
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
// yield* Effect.promise(() =>
|
||||
// this.toLocalNode.write({
|
||||
// action: "known",
|
||||
// ...chunkToKnownState(newContent.id, coValue),
|
||||
// isCorrection: true,
|
||||
// })
|
||||
// );
|
||||
|
||||
return coValues;
|
||||
} else {
|
||||
// console.log("Appending to WAL", newContent.id);
|
||||
yield* this.withWAL((wal) =>
|
||||
writeToWal(
|
||||
wal,
|
||||
this.fs,
|
||||
newContent.id,
|
||||
newContentAsChunk,
|
||||
),
|
||||
);
|
||||
|
||||
return { ...coValues, [newContent.id]: merged.left };
|
||||
}
|
||||
result = nextChunk;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
loadCoValue<WH, RH, FS extends FileSystem<WH, RH>>(
|
||||
id: RawCoID,
|
||||
fs: FS,
|
||||
): Effect.Effect<CoValueChunk | undefined, FSErr> {
|
||||
// return _loadChunkFromWal(id, fs);
|
||||
return Effect.gen(this, function* () {
|
||||
const files = this.fileCache || (yield* fs.listFiles());
|
||||
this.fileCache = files;
|
||||
const blockFiles = files.filter((name) =>
|
||||
name.startsWith("hash_"),
|
||||
) as BlockFilename[];
|
||||
|
||||
for (const blockFile of blockFiles) {
|
||||
let cachedHeader:
|
||||
| { [id: RawCoID]: { start: number; length: number } }
|
||||
| undefined = this.headerCache.get(blockFile);
|
||||
|
||||
const { handle, size } = yield* fs.openToRead(blockFile);
|
||||
|
||||
// console.log("Attempting to load", id, blockFile);
|
||||
|
||||
if (!cachedHeader) {
|
||||
cachedHeader = {};
|
||||
const header = yield* readHeader(
|
||||
blockFile,
|
||||
handle,
|
||||
size,
|
||||
fs,
|
||||
);
|
||||
for (const entry of header) {
|
||||
cachedHeader[entry.id] = {
|
||||
start: entry.start,
|
||||
length: entry.length,
|
||||
};
|
||||
}
|
||||
|
||||
this.headerCache.set(blockFile, cachedHeader);
|
||||
}
|
||||
const headerEntry = cachedHeader[id];
|
||||
|
||||
// console.log("Header entry", id, headerEntry);
|
||||
|
||||
let result;
|
||||
if (headerEntry) {
|
||||
result = yield* readChunk(handle, headerEntry, fs);
|
||||
}
|
||||
|
||||
yield* fs.close(handle);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
// await fs.close(handle);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async compact() {
|
||||
await Effect.runPromise(
|
||||
Effect.gen(this, function* () {
|
||||
const fileNames = yield* this.fs.listFiles();
|
||||
const fileNames = await this.fs.listFiles();
|
||||
|
||||
const walFiles = fileNames.filter((name) =>
|
||||
name.startsWith("wal-"),
|
||||
) as WalFilename[];
|
||||
walFiles.sort();
|
||||
const walFiles = fileNames.filter((name) =>
|
||||
name.startsWith("wal-"),
|
||||
) as WalFilename[];
|
||||
walFiles.sort();
|
||||
|
||||
const coValues = new Map<RawCoID, CoValueChunk>();
|
||||
|
||||
console.log("Compacting WAL files", walFiles);
|
||||
if (walFiles.length === 0) return;
|
||||
|
||||
const oldWal = this.currentWal;
|
||||
this.currentWal = undefined;
|
||||
|
||||
if (oldWal) {
|
||||
await this.fs.close(oldWal);
|
||||
}
|
||||
|
||||
for (const fileName of walFiles) {
|
||||
const { handle, size }: { handle: RH; size: number } =
|
||||
await this.fs.openToRead(fileName);
|
||||
if (size === 0) {
|
||||
await this.fs.close(handle);
|
||||
continue;
|
||||
}
|
||||
const bytes = await this.fs.read(handle, 0, size);
|
||||
|
||||
const decoded = textDecoder.decode(bytes);
|
||||
const lines = decoded.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.length === 0) continue;
|
||||
const chunk = JSON.parse(line) as WalEntry;
|
||||
|
||||
const existingChunk = coValues.get(chunk.id);
|
||||
|
||||
if (existingChunk) {
|
||||
const merged = mergeChunks(existingChunk, chunk);
|
||||
if (merged === "nonContigous") {
|
||||
console.log(
|
||||
"Non-contigous chunks in " +
|
||||
chunk.id +
|
||||
", " +
|
||||
fileName,
|
||||
existingChunk,
|
||||
chunk,
|
||||
);
|
||||
} else {
|
||||
coValues.set(chunk.id, merged);
|
||||
}
|
||||
} else {
|
||||
coValues.set(chunk.id, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
await this.fs.close(handle);
|
||||
}
|
||||
|
||||
const highestBlockNumber = fileNames.reduce((acc, name) => {
|
||||
if (name.startsWith("L" + MAX_N_LEVELS)) {
|
||||
const num = parseInt(name.split("-")[1]!);
|
||||
if (num > acc) {
|
||||
return num;
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
console.log([...coValues.keys()], fileNames, highestBlockNumber);
|
||||
|
||||
await writeBlock(
|
||||
coValues,
|
||||
MAX_N_LEVELS,
|
||||
highestBlockNumber + 1,
|
||||
this.fs,
|
||||
);
|
||||
|
||||
for (const walFile of walFiles) {
|
||||
await this.fs.removeFile(walFile);
|
||||
}
|
||||
this.fileCache = undefined;
|
||||
|
||||
const fileNames2 = await this.fs.listFiles();
|
||||
|
||||
const blockFiles = (
|
||||
fileNames2.filter((name) => name.startsWith("L")) as BlockFilename[]
|
||||
).sort();
|
||||
|
||||
const blockFilesByLevelInOrder: {
|
||||
[level: number]: BlockFilename[];
|
||||
} = {};
|
||||
|
||||
for (const blockFile of blockFiles) {
|
||||
const level = parseInt(blockFile.split("-")[0]!.slice(1));
|
||||
if (!blockFilesByLevelInOrder[level]) {
|
||||
blockFilesByLevelInOrder[level] = [];
|
||||
}
|
||||
blockFilesByLevelInOrder[level]!.push(blockFile);
|
||||
}
|
||||
|
||||
console.log(blockFilesByLevelInOrder);
|
||||
|
||||
for (let level = MAX_N_LEVELS; level > 0; level--) {
|
||||
const nBlocksDesired = Math.pow(2, level);
|
||||
const blocksInLevel = blockFilesByLevelInOrder[level];
|
||||
|
||||
if (blocksInLevel && blocksInLevel.length > nBlocksDesired) {
|
||||
console.log("Compacting blocks in level", level, blocksInLevel);
|
||||
|
||||
const coValues = new Map<RawCoID, CoValueChunk>();
|
||||
|
||||
yield* Effect.log("Compacting WAL files", walFiles);
|
||||
if (walFiles.length === 0) return;
|
||||
|
||||
yield* SynchronizedRef.updateEffect(this.currentWal, (wal) =>
|
||||
Effect.gen(this, function* () {
|
||||
if (wal) {
|
||||
yield* this.fs.close(wal);
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
);
|
||||
|
||||
for (const fileName of walFiles) {
|
||||
for (const blockFile of blocksInLevel) {
|
||||
const { handle, size }: { handle: RH; size: number } =
|
||||
yield* this.fs.openToRead(fileName);
|
||||
await this.getBlockHandle(blockFile, this.fs);
|
||||
|
||||
if (size === 0) {
|
||||
yield* this.fs.close(handle);
|
||||
continue;
|
||||
}
|
||||
const bytes = yield* this.fs.read(handle, 0, size);
|
||||
const header = await readHeader(
|
||||
blockFile,
|
||||
handle,
|
||||
size,
|
||||
this.fs,
|
||||
);
|
||||
for (const entry of header) {
|
||||
const chunk = await readChunk(handle, entry, this.fs);
|
||||
|
||||
const decoded = textDecoder.decode(bytes);
|
||||
const lines = decoded.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.length === 0) continue;
|
||||
const chunk = JSON.parse(line) as WalEntry;
|
||||
|
||||
const existingChunk = coValues.get(chunk.id);
|
||||
const existingChunk = coValues.get(entry.id);
|
||||
|
||||
if (existingChunk) {
|
||||
const merged = mergeChunks(existingChunk, chunk);
|
||||
if (Either.isRight(merged)) {
|
||||
yield* Effect.logWarning(
|
||||
if (merged === "nonContigous") {
|
||||
console.log(
|
||||
"Non-contigous chunks in " +
|
||||
chunk.id +
|
||||
entry.id +
|
||||
", " +
|
||||
fileName,
|
||||
blockFile,
|
||||
existingChunk,
|
||||
chunk,
|
||||
);
|
||||
} else {
|
||||
coValues.set(chunk.id, merged.left);
|
||||
coValues.set(entry.id, merged);
|
||||
}
|
||||
} else {
|
||||
coValues.set(chunk.id, chunk);
|
||||
coValues.set(entry.id, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
yield* this.fs.close(handle);
|
||||
}
|
||||
|
||||
yield* writeBlock(coValues, 0, this.fs);
|
||||
for (const walFile of walFiles) {
|
||||
yield* this.fs.removeFile(walFile);
|
||||
let levelBelow = blockFilesByLevelInOrder[level - 1];
|
||||
if (!levelBelow) {
|
||||
levelBelow = [];
|
||||
blockFilesByLevelInOrder[level - 1] = levelBelow;
|
||||
}
|
||||
this.fileCache = undefined;
|
||||
}),
|
||||
|
||||
const highestBlockNumberInLevelBelow = levelBelow.reduce(
|
||||
(acc, name) => {
|
||||
const num = parseInt(name.split("-")[1]!);
|
||||
if (num > acc) {
|
||||
return num;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
const newBlockName = await writeBlock(
|
||||
coValues,
|
||||
level - 1,
|
||||
highestBlockNumberInLevelBelow + 1,
|
||||
this.fs,
|
||||
);
|
||||
levelBelow.push(newBlockName);
|
||||
|
||||
// delete blocks that went into this one
|
||||
for (const blockFile of blocksInLevel) {
|
||||
const handle = await this.getBlockHandle(
|
||||
blockFile,
|
||||
this.fs,
|
||||
);
|
||||
await this.fs.close(handle.handle);
|
||||
await this.fs.removeFile(blockFile);
|
||||
this.blockFileHandles.delete(blockFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(
|
||||
() =>
|
||||
this.compact().catch((e) => {
|
||||
console.error("Error while compacting", e);
|
||||
}),
|
||||
5000,
|
||||
);
|
||||
|
||||
setTimeout(() => this.compact(), 5000);
|
||||
}
|
||||
|
||||
static async asPeer<WH, RH, FS extends FileSystem<WH, RH>>({
|
||||
static asPeer<WH, RH, FS extends FileSystem<WH, RH>>({
|
||||
fs,
|
||||
trace,
|
||||
localNodeName = "local",
|
||||
@@ -453,13 +554,15 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
|
||||
fs: FS;
|
||||
trace?: boolean;
|
||||
localNodeName?: string;
|
||||
}): Promise<Peer> {
|
||||
const [localNodeAsPeer, storageAsPeer] = await Effect.runPromise(
|
||||
connectedPeers(localNodeName, "storage", {
|
||||
}): Peer {
|
||||
const [localNodeAsPeer, storageAsPeer] = connectedPeers(
|
||||
localNodeName,
|
||||
"storage",
|
||||
{
|
||||
peer1role: "client",
|
||||
peer2role: "server",
|
||||
trace,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
new LSMStorage(fs, localNodeAsPeer.incoming, localNodeAsPeer.outgoing);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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,
|
||||
@@ -13,54 +14,57 @@ export function connectedPeers(
|
||||
peer1role?: Peer["role"];
|
||||
peer2role?: Peer["role"];
|
||||
} = {},
|
||||
): 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,
|
||||
);
|
||||
): [Peer, Peer] {
|
||||
const [from1to2Rx, from1to2Tx] = newQueuePair(
|
||||
trace ? { traceAs: `${peer1id} -> ${peer2id}` } : undefined,
|
||||
);
|
||||
const [from2to1Rx, from2to1Tx] = newQueuePair(
|
||||
trace ? { traceAs: `${peer2id} -> ${peer1id}` } : undefined,
|
||||
);
|
||||
|
||||
const peer2AsPeer: Peer = {
|
||||
id: peer2id,
|
||||
incoming: from2to1Rx,
|
||||
outgoing: from1to2Tx,
|
||||
role: peer2role,
|
||||
};
|
||||
const peer2AsPeer: Peer = {
|
||||
id: peer2id,
|
||||
incoming: from2to1Rx,
|
||||
outgoing: from1to2Tx,
|
||||
role: peer2role,
|
||||
};
|
||||
|
||||
const peer1AsPeer: Peer = {
|
||||
id: peer1id,
|
||||
incoming: from1to2Rx,
|
||||
outgoing: from2to1Tx,
|
||||
role: peer1role,
|
||||
};
|
||||
const peer1AsPeer: Peer = {
|
||||
id: peer1id,
|
||||
incoming: from1to2Rx,
|
||||
outgoing: from2to1Tx,
|
||||
role: peer1role,
|
||||
};
|
||||
|
||||
return [peer1AsPeer, peer2AsPeer];
|
||||
});
|
||||
return [peer1AsPeer, peer2AsPeer];
|
||||
}
|
||||
|
||||
export function newQueuePair(
|
||||
options: { traceAs?: string } = {},
|
||||
): Effect.Effect<[Stream.Stream<SyncMessage>, Queue.Enqueue<SyncMessage>]> {
|
||||
return Effect.gen(function* () {
|
||||
const queue = yield* Queue.unbounded<SyncMessage>();
|
||||
): [AsyncIterable<SyncMessage>, Channel<SyncMessage>] {
|
||||
const channel = new Channel<SyncMessage>();
|
||||
|
||||
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,
|
||||
),
|
||||
))), queue];
|
||||
} else {
|
||||
return [Stream.fromQueue(queue), queue];
|
||||
}
|
||||
});
|
||||
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,
|
||||
),
|
||||
);
|
||||
yield msg;
|
||||
}
|
||||
})(),
|
||||
channel,
|
||||
];
|
||||
} else {
|
||||
return [channel.wrap(), channel];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { CoValueHeader, Transaction } from "./coValueCore.js";
|
||||
import { CoValueCore } from "./coValueCore.js";
|
||||
import { LocalNode, newLoadingState } from "./localNode.js";
|
||||
import { RawCoID, SessionID } from "./ids.js";
|
||||
import { Effect, Queue, Stream } from "effect";
|
||||
|
||||
export type CoValueKnownState = {
|
||||
id: RawCoID;
|
||||
@@ -56,22 +55,17 @@ export type DoneMessage = {
|
||||
|
||||
export type PeerID = string;
|
||||
|
||||
export class DisconnectedError extends Error {
|
||||
readonly _tag = "DisconnectedError";
|
||||
constructor(public message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
export type DisconnectedError = "Disconnected";
|
||||
|
||||
export class PingTimeoutError extends Error {
|
||||
readonly _tag = "PingTimeoutError";
|
||||
}
|
||||
export type PingTimeoutError = "PingTimeout";
|
||||
|
||||
export type IncomingSyncStream = Stream.Stream<
|
||||
SyncMessage,
|
||||
DisconnectedError | PingTimeoutError
|
||||
export type IncomingSyncStream = AsyncIterable<
|
||||
SyncMessage | DisconnectedError | PingTimeoutError
|
||||
>;
|
||||
export type OutgoingSyncQueue = Queue.Enqueue<SyncMessage>;
|
||||
export type OutgoingSyncQueue = {
|
||||
push: (msg: SyncMessage) => Promise<unknown>;
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
export interface Peer {
|
||||
id: PeerID;
|
||||
@@ -147,15 +141,11 @@ export class SyncManager {
|
||||
|
||||
for (const peer of eligiblePeers) {
|
||||
// console.log("loading", id, "from", peer.id);
|
||||
Effect.runPromise(
|
||||
Queue.offer(peer.outgoing, {
|
||||
action: "load",
|
||||
id: id,
|
||||
header: false,
|
||||
sessions: {},
|
||||
}),
|
||||
).catch((e) => {
|
||||
console.error("Error writing to peer", e);
|
||||
await peer.outgoing.push({
|
||||
action: "load",
|
||||
id: id,
|
||||
header: false,
|
||||
sessions: {},
|
||||
});
|
||||
|
||||
const coValueEntry = this.local.coValues[id];
|
||||
@@ -232,11 +222,13 @@ export class SyncManager {
|
||||
}
|
||||
|
||||
if (entry.state === "loading") {
|
||||
await this.trySendToPeer(peer, {
|
||||
this.trySendToPeer(peer, {
|
||||
action: "load",
|
||||
id,
|
||||
header: false,
|
||||
sessions: {},
|
||||
}).catch((e) => {
|
||||
console.error("Error sending load", e);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -249,9 +241,11 @@ export class SyncManager {
|
||||
|
||||
if (!peer.toldKnownState.has(id)) {
|
||||
peer.toldKnownState.add(id);
|
||||
await this.trySendToPeer(peer, {
|
||||
this.trySendToPeer(peer, {
|
||||
action: "load",
|
||||
...coValue.knownState(),
|
||||
}).catch((e) => {
|
||||
console.error("Error sending load", e);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -276,10 +270,12 @@ export class SyncManager {
|
||||
);
|
||||
|
||||
if (!peer.toldKnownState.has(id)) {
|
||||
await this.trySendToPeer(peer, {
|
||||
this.trySendToPeer(peer, {
|
||||
action: "known",
|
||||
asDependencyOf,
|
||||
...coValue.knownState(),
|
||||
}).catch((e) => {
|
||||
console.error("Error sending known state", e);
|
||||
});
|
||||
|
||||
peer.toldKnownState.add(id);
|
||||
@@ -314,7 +310,9 @@ export class SyncManager {
|
||||
// } header: ${!!piece.header}`,
|
||||
// // Object.values(piece.new).map((s) => s.newTransactions)
|
||||
// );
|
||||
await this.trySendToPeer(peer, piece);
|
||||
this.trySendToPeer(peer, piece).catch((e) => {
|
||||
console.error("Error sending content piece", e);
|
||||
});
|
||||
if (performance.now() - lastYield > 10) {
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
@@ -368,55 +366,39 @@ export class SyncManager {
|
||||
void initialSync();
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
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().catch((e) => {
|
||||
console.error("Error processing messages from peer", peer.id, e);
|
||||
});
|
||||
}
|
||||
|
||||
trySendToPeer(peer: PeerState, msg: SyncMessage) {
|
||||
return Effect.runPromise(Queue.offer(peer.outgoing, msg));
|
||||
return peer.outgoing.push(msg);
|
||||
}
|
||||
|
||||
async handleLoad(msg: LoadMessage, peer: PeerState) {
|
||||
@@ -429,7 +411,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 && peer.role === "server",
|
||||
(other) => other.id !== peer.id && other.role === "server",
|
||||
);
|
||||
if (eligiblePeers.length === 0) {
|
||||
if (msg.header || Object.keys(msg.sessions).length > 0) {
|
||||
@@ -442,7 +424,7 @@ export class SyncManager {
|
||||
header: false,
|
||||
sessions: {},
|
||||
}).catch((e) => {
|
||||
console.error("Error sending known state back", e);
|
||||
console.error("Error sending known state", e);
|
||||
});
|
||||
}
|
||||
return;
|
||||
@@ -468,16 +450,18 @@ export class SyncManager {
|
||||
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);
|
||||
|
||||
await this.trySendToPeer(peer, {
|
||||
this.trySendToPeer(peer, {
|
||||
action: "known",
|
||||
id: msg.id,
|
||||
header: false,
|
||||
sessions: {},
|
||||
}).catch((e) => {
|
||||
console.error("Error sending known state back", e);
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -687,10 +671,12 @@ export class SyncManager {
|
||||
await this.syncCoValue(coValue);
|
||||
|
||||
if (invalidStateAssumed) {
|
||||
await this.trySendToPeer(peer, {
|
||||
this.trySendToPeer(peer, {
|
||||
action: "known",
|
||||
isCorrection: true,
|
||||
...coValue.knownState(),
|
||||
}).catch((e) => {
|
||||
console.error("Error sending known state correction", e);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -758,6 +744,12 @@ export class SyncManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gracefulShutdown() {
|
||||
for (const peer of Object.values(this.peers)) {
|
||||
peer.outgoing.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function knownStateIn(msg: LoadMessage | KnownStateMessage) {
|
||||
|
||||
@@ -3,7 +3,6 @@ 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();
|
||||
|
||||
@@ -53,13 +52,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] = await Effect.runPromise(connectedPeers("node1", "node2", {
|
||||
const [node1asPeer, node2asPeer] = connectedPeers("node1", "node2", {
|
||||
trace: true,
|
||||
peer1role: "server",
|
||||
peer2role: "client",
|
||||
}));
|
||||
})
|
||||
|
||||
console.log("After connected peers")
|
||||
console.log("After connected peers");
|
||||
|
||||
node.syncManager.addPeer(node2asPeer);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,87 @@
|
||||
# jazz-browser-media-images
|
||||
|
||||
## 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
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.25
|
||||
- jazz-browser@0.7.25
|
||||
|
||||
## 0.7.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.24
|
||||
- jazz-browser@0.7.24
|
||||
|
||||
## 0.7.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.23
|
||||
- jazz-browser@0.7.23
|
||||
|
||||
## 0.7.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.7.22
|
||||
|
||||
## 0.7.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.21
|
||||
- jazz-browser@0.7.21
|
||||
|
||||
## 0.7.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.20
|
||||
- jazz-browser@0.7.20
|
||||
|
||||
## 0.7.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.19
|
||||
- jazz-browser@0.7.19
|
||||
|
||||
## 0.7.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-browser-media-images",
|
||||
"version": "0.7.18",
|
||||
"version": "0.7.29",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -1,5 +1,95 @@
|
||||
# jazz-browser
|
||||
|
||||
## 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
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.25
|
||||
|
||||
## 0.7.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.24
|
||||
|
||||
## 0.7.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.23
|
||||
- jazz-tools@0.7.23
|
||||
- cojson-storage-indexeddb@0.7.23
|
||||
- cojson-transport-ws@0.7.23
|
||||
|
||||
## 0.7.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson-transport-ws@0.7.22
|
||||
|
||||
## 0.7.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.21
|
||||
|
||||
## 0.7.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.20
|
||||
|
||||
## 0.7.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.19
|
||||
|
||||
## 0.7.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-browser",
|
||||
"version": "0.7.18",
|
||||
"version": "0.7.29",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
@@ -10,7 +10,6 @@
|
||||
"cojson": "workspace:*",
|
||||
"cojson-storage-indexeddb": "workspace:*",
|
||||
"cojson-transport-ws": "workspace:*",
|
||||
"effect": "^3.5.2",
|
||||
"jazz-tools": "workspace:*",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import {
|
||||
BlockFilename,
|
||||
FSErr,
|
||||
FileSystem,
|
||||
WalFilename,
|
||||
CryptoProvider,
|
||||
} from "cojson";
|
||||
import { Effect } from "effect";
|
||||
import { BlockFilename, FileSystem, WalFilename, CryptoProvider } from "cojson";
|
||||
|
||||
export class OPFSFilesystem implements FileSystem<number, number> {
|
||||
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;
|
||||
@@ -28,18 +27,18 @@ export class OPFSFilesystem implements FileSystem<number, number> {
|
||||
};
|
||||
}
|
||||
|
||||
listFiles(): Effect.Effect<string[], FSErr, never> {
|
||||
return Effect.async((cb) => {
|
||||
listFiles(): Promise<string[]> {
|
||||
return new Promise((resolve) => {
|
||||
const requestId = this.nextRequestId++;
|
||||
performance.mark("listFiles" + requestId);
|
||||
performance.mark("listFiles" + requestId + "_listFiles");
|
||||
this.callbacks.set(requestId, (event) => {
|
||||
performance.mark("listFilesEnd" + requestId);
|
||||
performance.mark("listFilesEnd" + requestId + "_listFiles");
|
||||
performance.measure(
|
||||
"listFiles" + requestId,
|
||||
"listFiles" + requestId,
|
||||
"listFilesEnd" + requestId,
|
||||
"listFiles" + requestId + "_listFiles",
|
||||
"listFiles" + requestId + "_listFiles",
|
||||
"listFilesEnd" + requestId + "_listFiles",
|
||||
);
|
||||
cb(Effect.succeed(event.data.fileNames));
|
||||
resolve(event.data.fileNames);
|
||||
});
|
||||
this.opfsWorker.postMessage({ type: "listFiles", requestId });
|
||||
});
|
||||
@@ -47,22 +46,20 @@ export class OPFSFilesystem implements FileSystem<number, number> {
|
||||
|
||||
openToRead(
|
||||
filename: string,
|
||||
): Effect.Effect<{ handle: number; size: number }, FSErr, never> {
|
||||
return Effect.async((cb) => {
|
||||
): Promise<{ handle: { id: number; filename: string }; size: number }> {
|
||||
return new Promise((resolve) => {
|
||||
const requestId = this.nextRequestId++;
|
||||
performance.mark("openToRead" + requestId);
|
||||
performance.mark("openToRead" + "_" + filename);
|
||||
this.callbacks.set(requestId, (event) => {
|
||||
cb(
|
||||
Effect.succeed({
|
||||
handle: event.data.handle,
|
||||
size: event.data.size,
|
||||
}),
|
||||
);
|
||||
performance.mark("openToReadEnd" + requestId);
|
||||
resolve({
|
||||
handle: { id: event.data.handle, filename },
|
||||
size: event.data.size,
|
||||
});
|
||||
performance.mark("openToReadEnd" + "_" + filename);
|
||||
performance.measure(
|
||||
"openToRead" + requestId,
|
||||
"openToRead" + requestId,
|
||||
"openToReadEnd" + requestId,
|
||||
"openToRead" + "_" + filename,
|
||||
"openToRead" + "_" + filename,
|
||||
"openToReadEnd" + "_" + filename,
|
||||
);
|
||||
});
|
||||
this.opfsWorker.postMessage({
|
||||
@@ -73,18 +70,18 @@ export class OPFSFilesystem implements FileSystem<number, number> {
|
||||
});
|
||||
}
|
||||
|
||||
createFile(filename: string): Effect.Effect<number, FSErr, never> {
|
||||
return Effect.async((cb) => {
|
||||
createFile(filename: string): Promise<{ id: number; filename: string }> {
|
||||
return new Promise((resolve) => {
|
||||
const requestId = this.nextRequestId++;
|
||||
performance.mark("createFile" + requestId);
|
||||
performance.mark("createFile" + "_" + filename);
|
||||
this.callbacks.set(requestId, (event) => {
|
||||
performance.mark("createFileEnd" + requestId);
|
||||
performance.mark("createFileEnd" + "_" + filename);
|
||||
performance.measure(
|
||||
"createFile" + requestId,
|
||||
"createFile" + requestId,
|
||||
"createFileEnd" + requestId,
|
||||
"createFile" + "_" + filename,
|
||||
"createFile" + "_" + filename,
|
||||
"createFileEnd" + "_" + filename,
|
||||
);
|
||||
cb(Effect.succeed(event.data.handle));
|
||||
resolve({ id: event.data.handle, filename });
|
||||
});
|
||||
this.opfsWorker.postMessage({
|
||||
type: "createFile",
|
||||
@@ -94,20 +91,18 @@ export class OPFSFilesystem implements FileSystem<number, number> {
|
||||
});
|
||||
}
|
||||
|
||||
openToWrite(
|
||||
filename: string,
|
||||
): Effect.Effect<FileSystemFileHandle, FSErr, never> {
|
||||
return Effect.async((cb) => {
|
||||
openToWrite(filename: string): Promise<{ id: number; filename: string }> {
|
||||
return new Promise((resolve) => {
|
||||
const requestId = this.nextRequestId++;
|
||||
performance.mark("openToWrite" + requestId);
|
||||
performance.mark("openToWrite" + "_" + filename);
|
||||
this.callbacks.set(requestId, (event) => {
|
||||
performance.mark("openToWriteEnd" + requestId);
|
||||
performance.mark("openToWriteEnd" + "_" + filename);
|
||||
performance.measure(
|
||||
"openToWrite" + requestId,
|
||||
"openToWrite" + requestId,
|
||||
"openToWriteEnd" + requestId,
|
||||
"openToWrite" + "_" + filename,
|
||||
"openToWrite" + "_" + filename,
|
||||
"openToWriteEnd" + "_" + filename,
|
||||
);
|
||||
cb(Effect.succeed(event.data.handle));
|
||||
resolve({ id: event.data.handle, filename });
|
||||
});
|
||||
this.opfsWorker.postMessage({
|
||||
type: "openToWrite",
|
||||
@@ -118,24 +113,24 @@ export class OPFSFilesystem implements FileSystem<number, number> {
|
||||
}
|
||||
|
||||
append(
|
||||
handle: number,
|
||||
handle: { id: number; filename: string },
|
||||
data: Uint8Array,
|
||||
): Effect.Effect<void, FSErr, never> {
|
||||
return Effect.async((cb) => {
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const requestId = this.nextRequestId++;
|
||||
performance.mark("append" + requestId);
|
||||
performance.mark("append" + "_" + handle.filename);
|
||||
this.callbacks.set(requestId, (_) => {
|
||||
performance.mark("appendEnd" + requestId);
|
||||
performance.mark("appendEnd" + "_" + handle.filename);
|
||||
performance.measure(
|
||||
"append" + requestId,
|
||||
"append" + requestId,
|
||||
"appendEnd" + requestId,
|
||||
"append" + "_" + handle.filename,
|
||||
"append" + "_" + handle.filename,
|
||||
"appendEnd" + "_" + handle.filename,
|
||||
);
|
||||
cb(Effect.succeed(undefined));
|
||||
resolve(undefined);
|
||||
});
|
||||
this.opfsWorker.postMessage({
|
||||
type: "append",
|
||||
handle,
|
||||
handle: handle.id,
|
||||
data,
|
||||
requestId,
|
||||
});
|
||||
@@ -143,25 +138,25 @@ export class OPFSFilesystem implements FileSystem<number, number> {
|
||||
}
|
||||
|
||||
read(
|
||||
handle: number,
|
||||
handle: { id: number; filename: string },
|
||||
offset: number,
|
||||
length: number,
|
||||
): Effect.Effect<Uint8Array, FSErr, never> {
|
||||
return Effect.async((cb) => {
|
||||
): Promise<Uint8Array> {
|
||||
return new Promise((resolve) => {
|
||||
const requestId = this.nextRequestId++;
|
||||
performance.mark("read" + requestId);
|
||||
performance.mark("read" + "_" + handle.filename);
|
||||
this.callbacks.set(requestId, (event) => {
|
||||
performance.mark("readEnd" + requestId);
|
||||
performance.mark("readEnd" + "_" + handle.filename);
|
||||
performance.measure(
|
||||
"read" + requestId,
|
||||
"read" + requestId,
|
||||
"readEnd" + requestId,
|
||||
"read" + "_" + handle.filename,
|
||||
"read" + "_" + handle.filename,
|
||||
"readEnd" + "_" + handle.filename,
|
||||
);
|
||||
cb(Effect.succeed(event.data.data));
|
||||
resolve(event.data.data);
|
||||
});
|
||||
this.opfsWorker.postMessage({
|
||||
type: "read",
|
||||
handle,
|
||||
handle: handle.id,
|
||||
offset,
|
||||
length,
|
||||
requestId,
|
||||
@@ -169,66 +164,64 @@ export class OPFSFilesystem implements FileSystem<number, number> {
|
||||
});
|
||||
}
|
||||
|
||||
close(handle: number): Effect.Effect<void, FSErr, never> {
|
||||
return Effect.async((cb) => {
|
||||
close(handle: { id: number; filename: string }): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const requestId = this.nextRequestId++;
|
||||
performance.mark("close" + requestId);
|
||||
performance.mark("close" + "_" + handle.filename);
|
||||
this.callbacks.set(requestId, (_) => {
|
||||
performance.mark("closeEnd" + requestId);
|
||||
performance.mark("closeEnd" + "_" + handle.filename);
|
||||
performance.measure(
|
||||
"close" + requestId,
|
||||
"close" + requestId,
|
||||
"closeEnd" + requestId,
|
||||
"close" + "_" + handle.filename,
|
||||
"close" + "_" + handle.filename,
|
||||
"closeEnd" + "_" + handle.filename,
|
||||
);
|
||||
cb(Effect.succeed(undefined));
|
||||
resolve(undefined);
|
||||
});
|
||||
this.opfsWorker.postMessage({
|
||||
type: "close",
|
||||
handle,
|
||||
handle: handle.id,
|
||||
requestId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
closeAndRename(
|
||||
handle: number,
|
||||
handle: { id: number; filename: string },
|
||||
filename: BlockFilename,
|
||||
): Effect.Effect<void, FSErr, never> {
|
||||
return Effect.async((cb) => {
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const requestId = this.nextRequestId++;
|
||||
performance.mark("closeAndRename" + requestId);
|
||||
performance.mark("closeAndRename" + "_" + handle.filename);
|
||||
this.callbacks.set(requestId, () => {
|
||||
performance.mark("closeAndRenameEnd" + requestId);
|
||||
performance.mark("closeAndRenameEnd" + "_" + handle.filename);
|
||||
performance.measure(
|
||||
"closeAndRename" + requestId,
|
||||
"closeAndRename" + requestId,
|
||||
"closeAndRenameEnd" + requestId,
|
||||
"closeAndRename" + "_" + handle.filename,
|
||||
"closeAndRename" + "_" + handle.filename,
|
||||
"closeAndRenameEnd" + "_" + handle.filename,
|
||||
);
|
||||
cb(Effect.succeed(undefined));
|
||||
resolve(undefined);
|
||||
});
|
||||
this.opfsWorker.postMessage({
|
||||
type: "closeAndRename",
|
||||
handle,
|
||||
handle: handle.id,
|
||||
filename,
|
||||
requestId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeFile(
|
||||
filename: BlockFilename | WalFilename,
|
||||
): Effect.Effect<void, FSErr, never> {
|
||||
return Effect.async((cb) => {
|
||||
removeFile(filename: BlockFilename | WalFilename): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const requestId = this.nextRequestId++;
|
||||
performance.mark("removeFile" + requestId);
|
||||
performance.mark("removeFile" + "_" + filename);
|
||||
this.callbacks.set(requestId, () => {
|
||||
performance.mark("removeFileEnd" + requestId);
|
||||
performance.mark("removeFileEnd" + "_" + filename);
|
||||
performance.measure(
|
||||
"removeFile" + requestId,
|
||||
"removeFile" + requestId,
|
||||
"removeFileEnd" + requestId,
|
||||
"removeFile" + "_" + filename,
|
||||
"removeFile" + "_" + filename,
|
||||
"removeFileEnd" + "_" + filename,
|
||||
);
|
||||
cb(Effect.succeed(undefined));
|
||||
resolve(undefined);
|
||||
});
|
||||
this.opfsWorker.postMessage({
|
||||
type: "removeFile",
|
||||
|
||||
@@ -10,14 +10,10 @@ import {
|
||||
WasmCrypto,
|
||||
CryptoProvider,
|
||||
} from "jazz-tools";
|
||||
import {
|
||||
AccountID,
|
||||
LSMStorage,
|
||||
} from "cojson";
|
||||
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";
|
||||
|
||||
@@ -39,19 +35,17 @@ export async function createJazzBrowserContext<Acc extends Account>({
|
||||
auth: AuthProvider<Acc>;
|
||||
peer: `wss://${string}` | `ws://${string}`;
|
||||
reconnectionTimeout?: number;
|
||||
storage?: "indexedDB" | "experimentalOPFSdoNotUseOrYouWillBeFired";
|
||||
storage?: "indexedDB" | "singleTabOPFS";
|
||||
crypto?: CryptoProvider;
|
||||
}): Promise<BrowserContext<Acc>> {
|
||||
const crypto = customCrypto || (await WasmCrypto.create());
|
||||
let sessionDone: () => void;
|
||||
|
||||
const firstWsPeer = await Effect.runPromise(
|
||||
createWebSocketPeer({
|
||||
websocket: new WebSocket(peerAddr),
|
||||
id: peerAddr + "@" + new Date().toISOString(),
|
||||
role: "server",
|
||||
}),
|
||||
);
|
||||
const firstWsPeer = createWebSocketPeer({
|
||||
websocket: new WebSocket(peerAddr),
|
||||
id: peerAddr + "@" + new Date().toISOString(),
|
||||
role: "server",
|
||||
});
|
||||
let shouldTryToReconnect = true;
|
||||
|
||||
let currentReconnectionTimeout = initialReconnectionTimeout;
|
||||
@@ -115,13 +109,11 @@ export async function createJazzBrowserContext<Acc extends Account>({
|
||||
});
|
||||
|
||||
me._raw.core.node.syncManager.addPeer(
|
||||
await Effect.runPromise(
|
||||
createWebSocketPeer({
|
||||
websocket: new WebSocket(peerAddr),
|
||||
id: peerAddr + "@" + new Date().toISOString(),
|
||||
role: "server",
|
||||
}),
|
||||
),
|
||||
createWebSocketPeer({
|
||||
websocket: new WebSocket(peerAddr),
|
||||
id: peerAddr + "@" + new Date().toISOString(),
|
||||
role: "server",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -135,11 +127,7 @@ export async function createJazzBrowserContext<Acc extends Account>({
|
||||
shouldTryToReconnect = false;
|
||||
window.removeEventListener("online", onOnline);
|
||||
console.log("Cleaning up node");
|
||||
for (const peer of Object.values(
|
||||
me._raw.core.node.syncManager.peers,
|
||||
)) {
|
||||
void Effect.runPromise(Queue.shutdown(peer.outgoing));
|
||||
}
|
||||
me._raw.core.node.gracefulShutdown();
|
||||
sessionDone?.();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,91 @@
|
||||
# jazz-autosub
|
||||
|
||||
## 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
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.25
|
||||
|
||||
## 0.7.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.24
|
||||
|
||||
## 0.7.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.23
|
||||
- jazz-tools@0.7.23
|
||||
- cojson-transport-ws@0.7.23
|
||||
|
||||
## 0.7.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson-transport-ws@0.7.22
|
||||
|
||||
## 0.7.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.21
|
||||
|
||||
## 0.7.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.20
|
||||
|
||||
## 0.7.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.19
|
||||
|
||||
## 0.7.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
"types": "src/index.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.7.18",
|
||||
"version": "0.7.29",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:*",
|
||||
"cojson-transport-ws": "workspace:*",
|
||||
"effect": "^3.5.2",
|
||||
"jazz-tools": "workspace:*",
|
||||
"ws": "^8.14.2"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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 */
|
||||
@@ -18,13 +17,11 @@ export async function startWorker<Acc extends Account>({
|
||||
syncServer?: string;
|
||||
accountSchema?: CoValueClass<Acc> & typeof Account;
|
||||
}): Promise<{ worker: Acc }> {
|
||||
const wsPeer: Peer = await Effect.runPromise(
|
||||
createWebSocketPeer({
|
||||
id: "upstream",
|
||||
websocket: new WebSocket(peer),
|
||||
role: "server",
|
||||
}),
|
||||
);
|
||||
const wsPeer: Peer = createWebSocketPeer({
|
||||
id: "upstream",
|
||||
websocket: new WebSocket(peer),
|
||||
role: "server",
|
||||
});
|
||||
|
||||
if (!accountID) {
|
||||
throw new Error("No accountID provided");
|
||||
@@ -52,13 +49,11 @@ 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 = await Effect.runPromise(
|
||||
createWebSocketPeer({
|
||||
id: "upstream",
|
||||
websocket: new WebSocket(peer),
|
||||
role: "server",
|
||||
}),
|
||||
);
|
||||
const wsPeer: Peer = createWebSocketPeer({
|
||||
id: "upstream",
|
||||
websocket: new WebSocket(peer),
|
||||
role: "server",
|
||||
});
|
||||
|
||||
worker._raw.core.node.syncManager.addPeer(wsPeer);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,95 @@
|
||||
# jazz-react
|
||||
|
||||
## 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
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.25
|
||||
- jazz-browser@0.7.25
|
||||
|
||||
## 0.7.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.24
|
||||
- jazz-browser@0.7.24
|
||||
|
||||
## 0.7.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Mostly complete OPFS implementation (single-tab only)
|
||||
- Updated dependencies
|
||||
- cojson@0.7.23
|
||||
- jazz-tools@0.7.23
|
||||
- jazz-browser@0.7.23
|
||||
|
||||
## 0.7.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.7.22
|
||||
|
||||
## 0.7.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.21
|
||||
- jazz-browser@0.7.21
|
||||
|
||||
## 0.7.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.20
|
||||
- jazz-browser@0.7.20
|
||||
|
||||
## 0.7.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.19
|
||||
- jazz-browser@0.7.19
|
||||
|
||||
## 0.7.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-react",
|
||||
"version": "0.7.18",
|
||||
"version": "0.7.29",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -104,7 +104,10 @@ const DemoAuthBasicUI = ({
|
||||
signUp: (username: string) => void;
|
||||
}) => {
|
||||
const [username, setUsername] = useState<string>("");
|
||||
const darkMode = typeof window !== 'undefined' ? window.matchMedia("(prefers-color-scheme: dark)").matches : false;
|
||||
const darkMode =
|
||||
typeof window !== "undefined"
|
||||
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -23,7 +23,7 @@ export function createJazzReactContext<Acc extends Account>({
|
||||
}: {
|
||||
auth: ReactAuthHook<Acc>;
|
||||
peer: `wss://${string}` | `ws://${string}`;
|
||||
storage?: "indexedDB" | "experimentalOPFSdoNotUseOrYouWillBeFired";
|
||||
storage?: "indexedDB" | "singleTabOPFS";
|
||||
}): JazzReactContext<Acc> {
|
||||
const JazzContext = React.createContext<
|
||||
| {
|
||||
|
||||
@@ -1,5 +1,90 @@
|
||||
# jazz-autosub
|
||||
|
||||
## 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
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.25
|
||||
|
||||
## 0.7.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.24
|
||||
|
||||
## 0.7.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson@0.7.23
|
||||
- jazz-tools@0.7.23
|
||||
- cojson-transport-ws@0.7.23
|
||||
|
||||
## 0.7.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- cojson-transport-ws@0.7.22
|
||||
|
||||
## 0.7.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.21
|
||||
|
||||
## 0.7.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.20
|
||||
|
||||
## 0.7.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- jazz-tools@0.7.19
|
||||
|
||||
## 0.7.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"bin": "./dist/index.js",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.7.18",
|
||||
"version": "0.7.29",
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext ts,tsx",
|
||||
"format": "prettier --write './src/**/*.{ts,tsx}'",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Command, Options } from "@effect/cli";
|
||||
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
||||
import { Console, Effect } from "effect";
|
||||
import { createWebSocketPeer } from "cojson-transport-ws";
|
||||
import { WebSocket } from "ws"
|
||||
import { WebSocket } from "ws";
|
||||
import {
|
||||
Account,
|
||||
WasmCrypto,
|
||||
@@ -25,7 +25,7 @@ const accountCreate = Command.make(
|
||||
return Effect.gen(function* () {
|
||||
const crypto = yield* Effect.promise(() => WasmCrypto.create());
|
||||
|
||||
const peer = yield* createWebSocketPeer({
|
||||
const peer = createWebSocketPeer({
|
||||
id: "upstream",
|
||||
websocket: new WebSocket(peerAddr),
|
||||
role: "server",
|
||||
@@ -53,7 +53,7 @@ const accountCreate = Command.make(
|
||||
),
|
||||
);
|
||||
|
||||
const peer2 = yield* createWebSocketPeer({
|
||||
const peer2 = createWebSocketPeer({
|
||||
id: "upstream2",
|
||||
websocket: new WebSocket(peerAddr),
|
||||
role: "server",
|
||||
|
||||
@@ -1,5 +1,65 @@
|
||||
# jazz-autosub
|
||||
|
||||
## 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
|
||||
|
||||
- Implement applyDiff on CoMap to only update changed fields
|
||||
|
||||
## 0.7.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Remove effectful API for loading/subscribing
|
||||
|
||||
## 0.7.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Mostly complete OPFS implementation (single-tab only)
|
||||
- Updated dependencies
|
||||
- cojson@0.7.23
|
||||
|
||||
## 0.7.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix another bug in CoMap 'has' proxy trap
|
||||
|
||||
## 0.7.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix bug in CoMap 'has' trap
|
||||
|
||||
## 0.7.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Add support for "in" operator in CoMaps
|
||||
|
||||
## 0.7.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -5,11 +5,9 @@
|
||||
"types": "./src/index.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.7.18",
|
||||
"version": "0.7.29",
|
||||
"dependencies": {
|
||||
"@effect/schema": "^0.66.16",
|
||||
"cojson": "workspace:*",
|
||||
"effect": "^3.5.2",
|
||||
"fast-check": "^3.17.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -11,7 +11,6 @@ import type {
|
||||
RawControlledAccount,
|
||||
SessionID,
|
||||
} from "cojson";
|
||||
import { Context, Effect, Stream } from "effect";
|
||||
import {
|
||||
CoMap,
|
||||
CoValue,
|
||||
@@ -22,7 +21,6 @@ import {
|
||||
RefIfCoValue,
|
||||
DeeplyLoaded,
|
||||
DepthsIn,
|
||||
UnavailableError,
|
||||
} from "../internal.js";
|
||||
import {
|
||||
Group,
|
||||
@@ -34,9 +32,7 @@ import {
|
||||
inspect,
|
||||
subscriptionsScopes,
|
||||
loadCoValue,
|
||||
loadCoValueEf,
|
||||
subscribeToCoValue,
|
||||
subscribeToCoValueEf,
|
||||
ensureCoValueLoaded,
|
||||
subscribeToExistingCoValue,
|
||||
} from "../internal.js";
|
||||
@@ -224,11 +220,11 @@ export class Account extends CoValueBase implements CoValue {
|
||||
},
|
||||
) {
|
||||
// TODO: is there a cleaner way to do this?
|
||||
const connectedPeers = await Effect.runPromise(cojsonInternals.connectedPeers(
|
||||
const connectedPeers = cojsonInternals.connectedPeers(
|
||||
"creatingAccount",
|
||||
"createdAccount",
|
||||
{ peer1role: "server", peer2role: "client" },
|
||||
));
|
||||
);
|
||||
|
||||
as._raw.core.node.syncManager.addPeer(connectedPeers[1]);
|
||||
|
||||
@@ -284,15 +280,6 @@ export class Account extends CoValueBase implements CoValue {
|
||||
return loadCoValue(this, id, as, depth);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static loadEf<A extends Account, Depth>(
|
||||
this: CoValueClass<A>,
|
||||
id: ID<A>,
|
||||
depth: Depth & DepthsIn<A>,
|
||||
): Effect.Effect<DeeplyLoaded<A, Depth>, UnavailableError, AccountCtx> {
|
||||
return loadCoValueEf<A, Depth>(this, id, depth);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static subscribe<A extends Account, Depth>(
|
||||
this: CoValueClass<A>,
|
||||
@@ -304,15 +291,6 @@ export class Account extends CoValueBase implements CoValue {
|
||||
return subscribeToCoValue<A, Depth>(this, id, as, depth, listener);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static subscribeEf<A extends Account, Depth>(
|
||||
this: CoValueClass<A>,
|
||||
id: ID<A>,
|
||||
depth: Depth & DepthsIn<A>,
|
||||
): Stream.Stream<DeeplyLoaded<A, Depth>, UnavailableError, AccountCtx> {
|
||||
return subscribeToCoValueEf<A, Depth>(this, id, depth);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
ensureLoaded<A extends Account, Depth>(
|
||||
this: A,
|
||||
@@ -397,9 +375,6 @@ 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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { RawCoList } from "cojson";
|
||||
import type { JsonValue, RawCoList } from "cojson";
|
||||
import { RawAccount } from "cojson";
|
||||
import type {
|
||||
CoValue,
|
||||
@@ -10,8 +10,6 @@ import type {
|
||||
CoValueClass,
|
||||
DepthsIn,
|
||||
DeeplyLoaded,
|
||||
UnavailableError,
|
||||
AccountCtx,
|
||||
CoValueFromRaw,
|
||||
} from "../internal.js";
|
||||
import {
|
||||
@@ -25,14 +23,11 @@ import {
|
||||
inspect,
|
||||
isRefEncoded,
|
||||
loadCoValue,
|
||||
loadCoValueEf,
|
||||
makeRefs,
|
||||
subscribeToCoValue,
|
||||
subscribeToCoValueEf,
|
||||
subscribeToExistingCoValue,
|
||||
subscriptionsScopes,
|
||||
} from "../internal.js";
|
||||
import { encodeSync, decodeSync } from "@effect/schema/Schema";
|
||||
import { Effect, Stream } from "effect";
|
||||
|
||||
/**
|
||||
* CoLists are collaborative versions of plain arrays.
|
||||
@@ -308,7 +303,7 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
|
||||
} else if ("encoded" in itemDescriptor) {
|
||||
return this._raw
|
||||
.asArray()
|
||||
.map((e) => encodeSync(itemDescriptor.encoded)(e));
|
||||
.map((e) => itemDescriptor.encoded.encode(e));
|
||||
} else if (isRefEncoded(itemDescriptor)) {
|
||||
return this.map((item, idx) =>
|
||||
seenAbove?.includes((item as CoValue)?.id)
|
||||
@@ -376,21 +371,6 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
|
||||
return loadCoValue(this, id, as, depth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Effectful version of `CoList.load()`.
|
||||
*
|
||||
* Needs to be run inside an `AccountCtx` context.
|
||||
*
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
static loadEf<L extends CoList, Depth>(
|
||||
this: CoValueClass<L>,
|
||||
id: ID<L>,
|
||||
depth: Depth & DepthsIn<L>,
|
||||
): Effect.Effect<DeeplyLoaded<L, Depth>, UnavailableError, AccountCtx> {
|
||||
return loadCoValueEf<L, Depth>(this, id, depth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and subscribe to a `CoList` with a given ID, as a given account.
|
||||
*
|
||||
@@ -429,21 +409,6 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
|
||||
return subscribeToCoValue<L, Depth>(this, id, as, depth, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Effectful version of `CoList.subscribe()` that returns a stream of updates.
|
||||
*
|
||||
* Needs to be run inside an `AccountCtx` context.
|
||||
*
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
static subscribeEf<L extends CoList, Depth>(
|
||||
this: CoValueClass<L>,
|
||||
id: ID<L>,
|
||||
depth: Depth & DepthsIn<L>,
|
||||
): Stream.Stream<DeeplyLoaded<L, Depth>, UnavailableError, AccountCtx> {
|
||||
return subscribeToCoValueEf<L, Depth>(this, id, depth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an already loaded `CoList`, ensure that items are loaded to the specified depth.
|
||||
*
|
||||
@@ -479,16 +444,21 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
|
||||
castAs<Cl extends CoValueClass & CoValueFromRaw<CoValue>>(
|
||||
cl: Cl,
|
||||
): InstanceType<Cl> {
|
||||
return cl.fromRaw(this._raw) as InstanceType<Cl>;
|
||||
const casted = cl.fromRaw(this._raw) as InstanceType<Cl>;
|
||||
const subscriptionScope = subscriptionsScopes.get(this);
|
||||
if (subscriptionScope) {
|
||||
subscriptionsScopes.set(casted, subscriptionScope);
|
||||
}
|
||||
return casted;
|
||||
}
|
||||
}
|
||||
|
||||
function toRawItems<Item>(items: Item[], itemDescriptor: Schema) {
|
||||
const rawItems =
|
||||
itemDescriptor === "json"
|
||||
? items
|
||||
? (items as JsonValue[])
|
||||
: "encoded" in itemDescriptor
|
||||
? items?.map((e) => encodeSync(itemDescriptor.encoded)(e))
|
||||
? items?.map((e) => itemDescriptor.encoded.encode(e))
|
||||
: isRefEncoded(itemDescriptor)
|
||||
? items?.map((v) => (v as unknown as CoValue).id)
|
||||
: (() => {
|
||||
@@ -507,7 +477,7 @@ const CoListProxyHandler: ProxyHandler<CoList> = {
|
||||
} else if ("encoded" in itemDescriptor) {
|
||||
return rawValue === undefined
|
||||
? undefined
|
||||
: decodeSync(itemDescriptor.encoded)(rawValue);
|
||||
: itemDescriptor.encoded.decode(rawValue);
|
||||
} else if (isRefEncoded(itemDescriptor)) {
|
||||
return rawValue === undefined
|
||||
? undefined
|
||||
@@ -540,7 +510,7 @@ const CoListProxyHandler: ProxyHandler<CoList> = {
|
||||
if (itemDescriptor === "json") {
|
||||
rawValue = value;
|
||||
} else if ("encoded" in itemDescriptor) {
|
||||
rawValue = encodeSync(itemDescriptor.encoded)(value);
|
||||
rawValue = itemDescriptor.encoded.encode(value);
|
||||
} else if (isRefEncoded(itemDescriptor)) {
|
||||
rawValue = value.id;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { JsonValue, RawCoMap } from "cojson";
|
||||
import type { Simplify } from "effect/Types";
|
||||
import { encodeSync, decodeSync } from "@effect/schema/Schema";
|
||||
import type {
|
||||
CoValue,
|
||||
Schema,
|
||||
@@ -11,8 +9,6 @@ import type {
|
||||
RefIfCoValue,
|
||||
DepthsIn,
|
||||
DeeplyLoaded,
|
||||
UnavailableError,
|
||||
AccountCtx,
|
||||
CoValueClass,
|
||||
} from "../internal.js";
|
||||
import {
|
||||
@@ -26,13 +22,10 @@ import {
|
||||
ItemsSym,
|
||||
isRefEncoded,
|
||||
loadCoValue,
|
||||
loadCoValueEf,
|
||||
subscribeToCoValue,
|
||||
subscribeToCoValueEf,
|
||||
ensureCoValueLoaded,
|
||||
subscribeToExistingCoValue,
|
||||
} from "../internal.js";
|
||||
import { Effect, Stream } from "effect";
|
||||
|
||||
type CoMapEdit<V> = {
|
||||
value?: V;
|
||||
@@ -41,6 +34,10 @@ 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.
|
||||
*
|
||||
@@ -152,7 +149,7 @@ export class CoMap extends CoValueBase implements CoValue {
|
||||
descriptor === "json"
|
||||
? rawEdit.value
|
||||
: "encoded" in descriptor
|
||||
? decodeSync(descriptor.encoded)(rawEdit.value)
|
||||
? descriptor.encoded.encode(rawEdit.value)
|
||||
: new Ref(
|
||||
rawEdit.value as ID<CoValue>,
|
||||
target._loadedAs,
|
||||
@@ -322,7 +319,7 @@ export class CoMap extends CoValueBase implements CoValue {
|
||||
rawInit[key] = (initValue as unknown as CoValue).id;
|
||||
}
|
||||
} else if ("encoded" in descriptor) {
|
||||
rawInit[key] = encodeSync(descriptor.encoded)(
|
||||
rawInit[key] = descriptor.encoded.encode(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
initValue as any,
|
||||
);
|
||||
@@ -391,21 +388,6 @@ export class CoMap extends CoValueBase implements CoValue {
|
||||
return loadCoValue(this, id, as, depth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Effectful version of `CoMap.load()`.
|
||||
*
|
||||
* Needs to be run inside an `AccountCtx` context.
|
||||
*
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
static loadEf<M extends CoMap, Depth>(
|
||||
this: CoValueClass<M>,
|
||||
id: ID<M>,
|
||||
depth: Depth & DepthsIn<M>,
|
||||
): Effect.Effect<DeeplyLoaded<M, Depth>, UnavailableError, AccountCtx> {
|
||||
return loadCoValueEf<M, Depth>(this, id, depth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and subscribe to a `CoMap` with a given ID, as a given account.
|
||||
*
|
||||
@@ -444,21 +426,6 @@ export class CoMap extends CoValueBase implements CoValue {
|
||||
return subscribeToCoValue<M, Depth>(this, id, as, depth, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Effectful version of `CoMap.subscribe()` that returns a stream of updates.
|
||||
*
|
||||
* Needs to be run inside an `AccountCtx` context.
|
||||
*
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
static subscribeEf<M extends CoMap, Depth>(
|
||||
this: CoValueClass<M>,
|
||||
id: ID<M>,
|
||||
depth: Depth & DepthsIn<M>,
|
||||
): Stream.Stream<DeeplyLoaded<M, Depth>, UnavailableError, AccountCtx> {
|
||||
return subscribeToCoValueEf<M, Depth>(this, id, depth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an already loaded `CoMap`, ensure that the specified fields are loaded to the specified depth.
|
||||
*
|
||||
@@ -489,6 +456,37 @@ export class CoMap extends CoValueBase implements CoValue {
|
||||
): () => void {
|
||||
return subscribeToExistingCoValue(this, depth, listener);
|
||||
}
|
||||
|
||||
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[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;
|
||||
const newId = (newValue as CoValue | undefined)?.id;
|
||||
if (currentId !== newId) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(this as any)[tKey] = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export type CoKeys<Map extends object> = Exclude<
|
||||
@@ -520,7 +518,7 @@ const CoMapProxyHandler: ProxyHandler<CoMap> = {
|
||||
} else if ("encoded" in descriptor) {
|
||||
return raw === undefined
|
||||
? undefined
|
||||
: decodeSync(descriptor.encoded)(raw);
|
||||
: descriptor.encoded.decode(raw);
|
||||
} else if (isRefEncoded(descriptor)) {
|
||||
return raw === undefined
|
||||
? undefined
|
||||
@@ -554,7 +552,7 @@ const CoMapProxyHandler: ProxyHandler<CoMap> = {
|
||||
if (descriptor === "json") {
|
||||
target._raw.set(key, value);
|
||||
} else if ("encoded" in descriptor) {
|
||||
target._raw.set(key, encodeSync(descriptor.encoded)(value));
|
||||
target._raw.set(key, descriptor.encoded.encode(value));
|
||||
} else if (isRefEncoded(descriptor)) {
|
||||
target._raw.set(key, value.id);
|
||||
subscriptionsScopes
|
||||
@@ -610,6 +608,16 @@ const CoMapProxyHandler: ProxyHandler<CoMap> = {
|
||||
}
|
||||
}
|
||||
},
|
||||
has(target, key) {
|
||||
const descriptor = (target._schema?.[key as keyof CoMap["_schema"]] ||
|
||||
target._schema?.[ItemsSym]) as Schema;
|
||||
|
||||
if (target._raw && typeof key === "string" && descriptor) {
|
||||
return target._raw.get(key) !== undefined;
|
||||
} else {
|
||||
return Reflect.has(target, key);
|
||||
}
|
||||
},
|
||||
deleteProperty(target, key) {
|
||||
const descriptor = (target._schema[key as keyof CoMap["_schema"]] ||
|
||||
target._schema[ItemsSym]) as Schema;
|
||||
|
||||
@@ -17,11 +17,9 @@ import type {
|
||||
ID,
|
||||
IfCo,
|
||||
UnCo,
|
||||
AccountCtx,
|
||||
CoValueClass,
|
||||
DeeplyLoaded,
|
||||
DepthsIn,
|
||||
UnavailableError,
|
||||
} from "../internal.js";
|
||||
import {
|
||||
ItemsSym,
|
||||
@@ -33,14 +31,10 @@ import {
|
||||
SchemaInit,
|
||||
isRefEncoded,
|
||||
loadCoValue,
|
||||
loadCoValueEf,
|
||||
subscribeToCoValue,
|
||||
subscribeToCoValueEf,
|
||||
ensureCoValueLoaded,
|
||||
subscribeToExistingCoValue,
|
||||
} from "../internal.js";
|
||||
import { encodeSync, decodeSync } from "@effect/schema/Schema";
|
||||
import { Effect, Stream } from "effect";
|
||||
|
||||
export type CoStreamEntry<Item> = SingleCoStreamEntry<Item> & {
|
||||
all: IterableIterator<SingleCoStreamEntry<Item>>;
|
||||
@@ -146,7 +140,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(encodeSync(itemDescriptor.encoded)(item));
|
||||
this._raw.push(itemDescriptor.encoded.encode(item));
|
||||
} else if (isRefEncoded(itemDescriptor)) {
|
||||
this._raw.push((item as unknown as CoValue).id);
|
||||
}
|
||||
@@ -158,7 +152,7 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
|
||||
itemDescriptor === "json"
|
||||
? (v: unknown) => v
|
||||
: "encoded" in itemDescriptor
|
||||
? encodeSync(itemDescriptor.encoded)
|
||||
? itemDescriptor.encoded.encode
|
||||
: (v: unknown) => v && (v as CoValue).id;
|
||||
|
||||
return {
|
||||
@@ -202,15 +196,6 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
|
||||
return loadCoValue(this, id, as, depth);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static loadEf<S extends CoStream, Depth>(
|
||||
this: CoValueClass<S>,
|
||||
id: ID<S>,
|
||||
depth: Depth & DepthsIn<S>,
|
||||
): Effect.Effect<DeeplyLoaded<S, Depth>, UnavailableError, AccountCtx> {
|
||||
return loadCoValueEf<S, Depth>(this, id, depth);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static subscribe<S extends CoStream, Depth>(
|
||||
this: CoValueClass<S>,
|
||||
@@ -222,15 +207,6 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
|
||||
return subscribeToCoValue<S, Depth>(this, id, as, depth, listener);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static subscribeEf<S extends CoStream, Depth>(
|
||||
this: CoValueClass<S>,
|
||||
id: ID<S>,
|
||||
depth: Depth & DepthsIn<S>,
|
||||
): Stream.Stream<DeeplyLoaded<S, Depth>, UnavailableError, AccountCtx> {
|
||||
return subscribeToCoValueEf<S, Depth>(this, id, depth);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
ensureLoaded<S extends CoStream, Depth>(
|
||||
this: S,
|
||||
@@ -270,7 +246,7 @@ function entryFromRawEntry<Item>(
|
||||
? (CoValue & Item) | null
|
||||
: Item;
|
||||
} else if ("encoded" in itemField) {
|
||||
return decodeSync(itemField.encoded)(rawEntry.value);
|
||||
return itemField.encoded.decode(rawEntry.value);
|
||||
} else if (isRefEncoded(itemField)) {
|
||||
return this.ref?.accessFrom(
|
||||
accessFrom,
|
||||
@@ -638,15 +614,6 @@ export class BinaryCoStream extends CoValueBase implements CoValue {
|
||||
return loadCoValue(this, id, as, depth);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static loadEf<B extends BinaryCoStream, Depth>(
|
||||
this: CoValueClass<B>,
|
||||
id: ID<B>,
|
||||
depth: Depth & DepthsIn<B>,
|
||||
): Effect.Effect<DeeplyLoaded<B, Depth>, UnavailableError, AccountCtx> {
|
||||
return loadCoValueEf<B, Depth>(this, id, depth);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static subscribe<B extends BinaryCoStream, Depth>(
|
||||
this: CoValueClass<B>,
|
||||
@@ -658,15 +625,6 @@ export class BinaryCoStream extends CoValueBase implements CoValue {
|
||||
return subscribeToCoValue<B, Depth>(this, id, as, depth, listener);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static subscribeEf<B extends BinaryCoStream, Depth>(
|
||||
this: CoValueClass<B>,
|
||||
id: ID<B>,
|
||||
depth: Depth & DepthsIn<B>,
|
||||
): Stream.Stream<DeeplyLoaded<B, Depth>, UnavailableError, AccountCtx> {
|
||||
return subscribeToCoValueEf<B, Depth>(this, id, depth);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
ensureLoaded<B extends BinaryCoStream, Depth>(
|
||||
this: B,
|
||||
|
||||
@@ -4,11 +4,9 @@ import type {
|
||||
ID,
|
||||
RefEncoded,
|
||||
Schema,
|
||||
AccountCtx,
|
||||
CoValueClass,
|
||||
DeeplyLoaded,
|
||||
DepthsIn,
|
||||
UnavailableError,
|
||||
} from "../internal.js";
|
||||
import {
|
||||
Account,
|
||||
@@ -20,13 +18,10 @@ import {
|
||||
AccountAndGroupProxyHandler,
|
||||
MembersSym,
|
||||
loadCoValue,
|
||||
loadCoValueEf,
|
||||
subscribeToCoValue,
|
||||
subscribeToCoValueEf,
|
||||
ensureCoValueLoaded,
|
||||
subscribeToExistingCoValue,
|
||||
} from "../internal.js";
|
||||
import { Effect, Stream } from "effect";
|
||||
|
||||
/** @category Identity & Permissions */
|
||||
export class Profile extends CoMap {
|
||||
@@ -198,15 +193,6 @@ export class Group extends CoValueBase implements CoValue {
|
||||
return loadCoValue(this, id, as, depth);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static loadEf<G extends Group, Depth>(
|
||||
this: CoValueClass<G>,
|
||||
id: ID<G>,
|
||||
depth: Depth & DepthsIn<G>,
|
||||
): Effect.Effect<DeeplyLoaded<G, Depth>, UnavailableError, AccountCtx> {
|
||||
return loadCoValueEf<G, Depth>(this, id, depth);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static subscribe<G extends Group, Depth>(
|
||||
this: CoValueClass<G>,
|
||||
@@ -218,15 +204,6 @@ export class Group extends CoValueBase implements CoValue {
|
||||
return subscribeToCoValue<G, Depth>(this, id, as, depth, listener);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static subscribeEf<G extends Group, Depth>(
|
||||
this: CoValueClass<G>,
|
||||
id: ID<G>,
|
||||
depth: Depth & DepthsIn<G>,
|
||||
): Stream.Stream<DeeplyLoaded<G, Depth>, UnavailableError, AccountCtx> {
|
||||
return subscribeToCoValueEf<G, Depth>(this, id, depth);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
ensureLoaded<G extends Group, Depth>(
|
||||
this: G,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Effect, Option, Sink, Stream } from "effect";
|
||||
import type { CojsonInternalTypes, RawCoValue } from "cojson";
|
||||
import { RawAccount } from "cojson";
|
||||
import type { DeeplyLoaded, DepthsIn, UnavailableError } from "../internal.js";
|
||||
import type { DeeplyLoaded, DepthsIn } from "../internal.js";
|
||||
import {
|
||||
Account,
|
||||
AccountCtx,
|
||||
Group,
|
||||
SubscriptionScope,
|
||||
Ref,
|
||||
@@ -124,7 +122,12 @@ export class CoValueBase implements CoValue {
|
||||
castAs<Cl extends CoValueClass & CoValueFromRaw<CoValue>>(
|
||||
cl: Cl,
|
||||
): InstanceType<Cl> {
|
||||
return cl.fromRaw(this._raw) as InstanceType<Cl>;
|
||||
const casted = cl.fromRaw(this._raw) as InstanceType<Cl>;
|
||||
const subscriptionScope = subscriptionsScopes.get(this);
|
||||
if (subscriptionScope) {
|
||||
subscriptionsScopes.set(casted, subscriptionScope);
|
||||
}
|
||||
return casted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,13 +137,22 @@ export function loadCoValue<V extends CoValue, Depth>(
|
||||
as: Account,
|
||||
depth: Depth & DepthsIn<V>,
|
||||
): Promise<DeeplyLoaded<V, Depth> | undefined> {
|
||||
return Effect.runPromise(
|
||||
loadCoValueEf(cls, id, depth).pipe(
|
||||
Effect.mapError(() => undefined),
|
||||
Effect.merge,
|
||||
Effect.provideService(AccountCtx, as),
|
||||
),
|
||||
);
|
||||
return new Promise((resolve) => {
|
||||
const unsubscribe = subscribeToCoValue(
|
||||
cls,
|
||||
id,
|
||||
as,
|
||||
depth,
|
||||
(value) => {
|
||||
resolve(value);
|
||||
unsubscribe();
|
||||
},
|
||||
() => {
|
||||
resolve(undefined);
|
||||
unsubscribe();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function ensureCoValueLoaded<V extends CoValue, Depth>(
|
||||
@@ -155,41 +167,46 @@ export function ensureCoValueLoaded<V extends CoValue, Depth>(
|
||||
);
|
||||
}
|
||||
|
||||
export function loadCoValueEf<V extends CoValue, Depth>(
|
||||
cls: CoValueClass<V>,
|
||||
id: ID<V>,
|
||||
depth: Depth & DepthsIn<V>,
|
||||
): Effect.Effect<DeeplyLoaded<V, Depth>, UnavailableError, AccountCtx> {
|
||||
return subscribeToCoValueEf(cls, id, depth).pipe(
|
||||
Stream.runHead,
|
||||
Effect.andThen(
|
||||
Effect.mapError((_noSuchElem) => "unavailable" as const),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function subscribeToCoValue<V extends CoValue, Depth>(
|
||||
cls: CoValueClass<V>,
|
||||
id: ID<V>,
|
||||
as: Account,
|
||||
depth: Depth & DepthsIn<V>,
|
||||
listener: (value: DeeplyLoaded<V, Depth>) => void,
|
||||
onUnavailable?: () => void,
|
||||
): () => void {
|
||||
void Effect.runPromise(
|
||||
Effect.provideService(
|
||||
subscribeToCoValueEf(cls, id, depth).pipe(
|
||||
Stream.run(
|
||||
Sink.forEach((update) =>
|
||||
Effect.sync(() => listener(update)),
|
||||
),
|
||||
),
|
||||
),
|
||||
AccountCtx,
|
||||
as,
|
||||
),
|
||||
);
|
||||
const ref = new Ref(id, as, { ref: cls, optional: false });
|
||||
|
||||
return function unsubscribe() {};
|
||||
let unsubscribed = false;
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
|
||||
ref.load()
|
||||
.then((value) => {
|
||||
if (!value) {
|
||||
onUnavailable && onUnavailable();
|
||||
return;
|
||||
}
|
||||
if (unsubscribed) return;
|
||||
const subscription = new SubscriptionScope(
|
||||
value,
|
||||
cls as CoValueClass<V> & CoValueFromRaw<V>,
|
||||
(update) => {
|
||||
if (fulfillsDepth(depth, update)) {
|
||||
listener(update as DeeplyLoaded<V, Depth>);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
unsubscribe = () => subscription.unsubscribeAll();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Failed to load / subscribe to CoValue", e);
|
||||
});
|
||||
|
||||
return function unsubscribeAtAnyPoint() {
|
||||
unsubscribed = true;
|
||||
unsubscribe && unsubscribe();
|
||||
};
|
||||
}
|
||||
|
||||
export function subscribeToExistingCoValue<V extends CoValue, Depth>(
|
||||
@@ -205,43 +222,3 @@ export function subscribeToExistingCoValue<V extends CoValue, Depth>(
|
||||
listener,
|
||||
);
|
||||
}
|
||||
|
||||
export function subscribeToCoValueEf<V extends CoValue, Depth>(
|
||||
cls: CoValueClass<V>,
|
||||
id: ID<V>,
|
||||
depth: Depth & DepthsIn<V>,
|
||||
): Stream.Stream<DeeplyLoaded<V, Depth>, UnavailableError, AccountCtx> {
|
||||
return AccountCtx.pipe(
|
||||
Effect.andThen((account) =>
|
||||
new Ref(id, account, {
|
||||
ref: cls,
|
||||
optional: false,
|
||||
}).loadEf(),
|
||||
),
|
||||
Stream.fromEffect,
|
||||
Stream.flatMap((value: V) =>
|
||||
Stream.asyncScoped<V, UnavailableError>((emit) =>
|
||||
Effect.gen(function* (_) {
|
||||
const subscription = new SubscriptionScope(
|
||||
value,
|
||||
cls as CoValueClass<V> & CoValueFromRaw<V>,
|
||||
(update) => void emit.single(update as V),
|
||||
);
|
||||
|
||||
yield* _(
|
||||
Effect.addFinalizer(() =>
|
||||
Effect.sync(() => subscription.unsubscribeAll()),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
Stream.filterMap((update: V) =>
|
||||
Option.fromNullable(
|
||||
fulfillsDepth(depth, update)
|
||||
? (update as DeeplyLoaded<V, Depth>)
|
||||
: undefined,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Effect } from "effect";
|
||||
import type { CoID, RawCoValue } from "cojson";
|
||||
import type {
|
||||
Account,
|
||||
@@ -6,7 +5,6 @@ import type {
|
||||
ID,
|
||||
RefEncoded,
|
||||
UnCo,
|
||||
UnavailableError,
|
||||
} from "../internal.js";
|
||||
import {
|
||||
instantiateRefEncoded,
|
||||
@@ -47,22 +45,6 @@ export class Ref<out V extends CoValue> {
|
||||
}
|
||||
}
|
||||
|
||||
loadEf() {
|
||||
return Effect.async<V, UnavailableError>((fulfill) => {
|
||||
this.loadHelper()
|
||||
.then((value) => {
|
||||
if (value === "unavailable") {
|
||||
fulfill(Effect.fail<UnavailableError>("unavailable"));
|
||||
} else {
|
||||
fulfill(Effect.succeed(value));
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
fulfill(Effect.die(e));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async loadHelper(options?: {
|
||||
onProgress: (p: number) => void;
|
||||
}): Promise<V | "unavailable"> {
|
||||
|
||||
@@ -5,7 +5,6 @@ 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 */
|
||||
@@ -17,6 +16,36 @@ export type IfCo<C, R> = C extends infer _A | infer B
|
||||
: never;
|
||||
export type UnCo<T> = T extends co<infer A> ? A : T;
|
||||
|
||||
const optional = {
|
||||
ref: optionalRef,
|
||||
json<T extends JsonValue>(): co<T | undefined> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return { [SchemaInit]: "json" satisfies Schema } as any;
|
||||
},
|
||||
encoded<T>(arg: OptionalEncoder<T>): co<T | undefined> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return { [SchemaInit]: { encoded: arg } satisfies Schema } as any;
|
||||
},
|
||||
string: {
|
||||
[SchemaInit]: "json" satisfies Schema,
|
||||
} as unknown as co<string | undefined>,
|
||||
number: {
|
||||
[SchemaInit]: "json" satisfies Schema,
|
||||
} as unknown as co<number | undefined>,
|
||||
boolean: {
|
||||
[SchemaInit]: "json" satisfies Schema,
|
||||
} as unknown as co<boolean | undefined>,
|
||||
null: {
|
||||
[SchemaInit]: "json" satisfies Schema,
|
||||
} as unknown as co<null | undefined>,
|
||||
literal<T extends (string | number | boolean)[]>(
|
||||
..._lit: T
|
||||
): co<T[number] | undefined> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return { [SchemaInit]: "json" satisfies Schema } as any;
|
||||
},
|
||||
};
|
||||
|
||||
/** @category Schema definition */
|
||||
export const co = {
|
||||
string: {
|
||||
@@ -48,8 +77,15 @@ export const co = {
|
||||
ref,
|
||||
items: ItemsSym as ItemsSym,
|
||||
members: MembersSym as MembersSym,
|
||||
optional,
|
||||
};
|
||||
|
||||
function optionalRef<C extends CoValueClass>(
|
||||
arg: C | ((_raw: InstanceType<C>["_raw"]) => C),
|
||||
): co<InstanceType<C> | null | undefined> {
|
||||
return ref(arg, { optional: true });
|
||||
}
|
||||
|
||||
function ref<C extends CoValueClass>(
|
||||
arg: C | ((_raw: InstanceType<C>["_raw"]) => C),
|
||||
): co<InstanceType<C> | null>;
|
||||
@@ -76,7 +112,7 @@ function ref<
|
||||
}
|
||||
|
||||
export type JsonEncoded = "json";
|
||||
export type EncodedAs<V> = { encoded: Encoder<V> };
|
||||
export type EncodedAs<V> = { encoded: Encoder<V> | OptionalEncoder<V> };
|
||||
export type RefEncoded<V extends CoValue> = {
|
||||
ref: CoValueClass<V> | ((raw: RawCoValue) => CoValueClass<V>);
|
||||
optional: boolean;
|
||||
@@ -115,27 +151,23 @@ export type SchemaFor<Field> = NonNullable<Field> extends CoValue
|
||||
? JsonEncoded
|
||||
: EncodedAs<NonNullable<Field>>;
|
||||
|
||||
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 Encoder<V> = {
|
||||
encode: (value: V) => JsonValue;
|
||||
decode: (value: JsonValue) => V;
|
||||
};
|
||||
export type OptionalEncoder<V> =
|
||||
| Encoder<V>
|
||||
| {
|
||||
encode: (value: V | undefined) => JsonValue;
|
||||
decode: (value: JsonValue) => V | undefined;
|
||||
};
|
||||
|
||||
export type Encoder<V> = EffectSchemaWithInputAndOutput<V, JsonValue>;
|
||||
|
||||
import { Date } from "@effect/schema/Schema";
|
||||
import { SchemaInit, ItemsSym, MembersSym } from "./symbols.js";
|
||||
|
||||
/** @category Schema definition */
|
||||
export const Encoders = {
|
||||
Date,
|
||||
Date: {
|
||||
encode: (value: Date) => value.toISOString(),
|
||||
decode: (value: JsonValue) => new Date(value as string),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -26,9 +26,4 @@ export { ImageDefinition } from "./internal.js";
|
||||
export { CoValueBase, type CoValueClass } from "./internal.js";
|
||||
export type { DepthsIn, DeeplyLoaded } from "./internal.js";
|
||||
|
||||
export {
|
||||
loadCoValue,
|
||||
loadCoValueEf,
|
||||
subscribeToCoValue,
|
||||
subscribeToCoValueEf,
|
||||
} from "./internal.js";
|
||||
export { loadCoValue, subscribeToCoValue } from "./internal.js";
|
||||
|
||||
@@ -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,11 +157,14 @@ describe("CoList resolution", async () => {
|
||||
test("Loading and availability", async () => {
|
||||
const { me, list } = await initNodeAndList();
|
||||
|
||||
const [initialAsPeer, secondPeer] = await Effect.runPromise(connectedPeers(
|
||||
const [initialAsPeer, secondPeer] = connectedPeers(
|
||||
"initial",
|
||||
"second",
|
||||
{ peer1role: "server", peer2role: "client" },
|
||||
));
|
||||
{
|
||||
peer1role: "server",
|
||||
peer2role: "client",
|
||||
},
|
||||
);
|
||||
if (!isControlledAccount(me)) {
|
||||
throw "me is not a controlled account";
|
||||
}
|
||||
@@ -216,11 +219,14 @@ describe("CoList resolution", async () => {
|
||||
test("Subscription & auto-resolution", async () => {
|
||||
const { me, list } = await initNodeAndList();
|
||||
|
||||
const [initialAsPeer, secondPeer] = await Effect.runPromise(connectedPeers(
|
||||
const [initialAsPeer, secondPeer] = connectedPeers(
|
||||
"initial",
|
||||
"second",
|
||||
{ peer1role: "server", peer2role: "client" },
|
||||
));
|
||||
{
|
||||
peer1role: "server",
|
||||
peer2role: "client",
|
||||
},
|
||||
);
|
||||
if (!isControlledAccount(me)) {
|
||||
throw "me is not a controlled account";
|
||||
}
|
||||
@@ -234,63 +240,52 @@ describe("CoList resolution", async () => {
|
||||
crypto: Crypto,
|
||||
});
|
||||
|
||||
await Effect.runPromise(
|
||||
Effect.gen(function* ($) {
|
||||
const queue = yield* $(Queue.unbounded<TestList>());
|
||||
const queue = new cojsonInternals.Channel();
|
||||
|
||||
TestList.subscribe(
|
||||
list.id,
|
||||
meOnSecondPeer,
|
||||
[],
|
||||
(subscribedList) => {
|
||||
console.log(
|
||||
"subscribedList?.[0]?.[0]?.[0]",
|
||||
subscribedList?.[0]?.[0]?.[0],
|
||||
);
|
||||
void Effect.runPromise(
|
||||
Queue.offer(queue, subscribedList),
|
||||
);
|
||||
},
|
||||
);
|
||||
TestList.subscribe(list.id, meOnSecondPeer, [], (subscribedList) => {
|
||||
console.log(
|
||||
"subscribedList?.[0]?.[0]?.[0]",
|
||||
subscribedList?.[0]?.[0]?.[0],
|
||||
);
|
||||
void queue.push(subscribedList);
|
||||
});
|
||||
|
||||
const update1 = yield* $(Queue.take(queue));
|
||||
expect(update1?.[0]).toBe(null);
|
||||
const update1 = (await queue.next()).value;
|
||||
expect(update1?.[0]).toBe(null);
|
||||
|
||||
const update2 = yield* $(Queue.take(queue));
|
||||
expect(update2?.[0]).toBeDefined();
|
||||
expect(update2?.[0]?.[0]).toBe(null);
|
||||
const update2 = (await queue.next()).value;
|
||||
expect(update2?.[0]).toBeDefined();
|
||||
expect(update2?.[0]?.[0]).toBe(null);
|
||||
|
||||
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");
|
||||
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");
|
||||
|
||||
update3[0]![0]![0] = "x";
|
||||
update3[0]![0]![0] = "x";
|
||||
|
||||
const update4 = yield* $(Queue.take(queue));
|
||||
expect(update4?.[0]?.[0]?.[0]).toBe("x");
|
||||
const update4 = (await queue.next()).value;
|
||||
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 = yield* $(Queue.take(queue));
|
||||
expect(update5?.[0]?.[0]?.[0]).toBe("y");
|
||||
expect(update5?.[0]?.[0]?.joined()).toBe("y,z");
|
||||
const update5 = (await queue.next()).value;
|
||||
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 = yield* $(Queue.take(queue));
|
||||
expect(update6?.[0]?.[0]?.[0]).toBe("w");
|
||||
}),
|
||||
);
|
||||
// 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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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,
|
||||
@@ -9,8 +8,8 @@ import {
|
||||
co,
|
||||
WasmCrypto,
|
||||
isControlledAccount,
|
||||
cojsonInternals,
|
||||
} from "../index.js";
|
||||
import { Schema } from "@effect/schema";
|
||||
|
||||
const Crypto = await WasmCrypto.create();
|
||||
|
||||
@@ -25,7 +24,11 @@ describe("Simple CoMap operations", async () => {
|
||||
_height = co.number;
|
||||
birthday = co.encoded(Encoders.Date);
|
||||
name? = co.string;
|
||||
nullable = co.encoded(Schema.NullOr(Schema.String));
|
||||
nullable = co.optional.encoded<string | undefined>({
|
||||
encode: (value: string | undefined) => value || null,
|
||||
decode: (value: unknown) => (value as string) || undefined,
|
||||
});
|
||||
optionalDate = co.optional.encoded(Encoders.Date);
|
||||
|
||||
get roughColor() {
|
||||
return this.color + "ish";
|
||||
@@ -41,7 +44,7 @@ describe("Simple CoMap operations", async () => {
|
||||
color: "red",
|
||||
_height: 10,
|
||||
birthday: birthday,
|
||||
nullable: null,
|
||||
nullable: undefined,
|
||||
},
|
||||
{ owner: me },
|
||||
);
|
||||
@@ -93,7 +96,9 @@ describe("Simple CoMap operations", async () => {
|
||||
expect(map._raw.get("_height")).toEqual(20);
|
||||
|
||||
map.nullable = "not null";
|
||||
map.nullable = null;
|
||||
map.nullable = undefined;
|
||||
delete map.nullable;
|
||||
map.nullable = undefined;
|
||||
|
||||
map.name = "Secret name";
|
||||
expect(map.name).toEqual("Secret name");
|
||||
@@ -106,6 +111,21 @@ describe("Simple CoMap operations", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("property existence", () => {
|
||||
class TestMap extends CoMap.Record(co.string) {}
|
||||
test("CoMap", () => {
|
||||
const map = TestMap.create(
|
||||
{ name: "test" },
|
||||
{
|
||||
owner: me,
|
||||
},
|
||||
);
|
||||
|
||||
expect("name" in map).toBe(true);
|
||||
expect("something" in map).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
class RecursiveMap extends CoMap {
|
||||
name = co.string;
|
||||
next?: co<RecursiveMap | null> = co.ref(RecursiveMap);
|
||||
@@ -264,12 +284,12 @@ describe("CoMap resolution", async () => {
|
||||
|
||||
test("Loading and availability", async () => {
|
||||
const { me, map } = await initNodeAndMap();
|
||||
const [initialAsPeer, secondPeer] = await Effect.runPromise(
|
||||
const [initialAsPeer, secondPeer] =
|
||||
connectedPeers("initial", "second", {
|
||||
peer1role: "server",
|
||||
peer2role: "client",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
if (!isControlledAccount(me)) {
|
||||
throw "me is not a controlled account";
|
||||
}
|
||||
@@ -335,12 +355,12 @@ describe("CoMap resolution", async () => {
|
||||
test("Subscription & auto-resolution", async () => {
|
||||
const { me, map } = await initNodeAndMap();
|
||||
|
||||
const [initialAsPeer, secondAsPeer] = await Effect.runPromise(
|
||||
const [initialAsPeer, secondAsPeer] =
|
||||
connectedPeers("initial", "second", {
|
||||
peer1role: "server",
|
||||
peer2role: "client",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
if (!isControlledAccount(me)) {
|
||||
throw "me is not a controlled account";
|
||||
}
|
||||
@@ -354,9 +374,8 @@ describe("CoMap resolution", async () => {
|
||||
crypto: Crypto,
|
||||
});
|
||||
|
||||
await Effect.runPromise(
|
||||
Effect.gen(function* ($) {
|
||||
const queue = yield* $(Queue.unbounded<TestMap>());
|
||||
|
||||
const queue = new cojsonInternals.Channel<TestMap>();
|
||||
|
||||
TestMap.subscribe(
|
||||
map.id,
|
||||
@@ -367,22 +386,20 @@ describe("CoMap resolution", async () => {
|
||||
"subscribedMap.nested?.twiceNested?.taste",
|
||||
subscribedMap.nested?.twiceNested?.taste,
|
||||
);
|
||||
void Effect.runPromise(
|
||||
Queue.offer(queue, subscribedMap),
|
||||
);
|
||||
void queue.push(subscribedMap);
|
||||
},
|
||||
);
|
||||
|
||||
const update1 = yield* $(Queue.take(queue));
|
||||
const update1 = (await queue.next()).value;
|
||||
expect(update1.nested).toEqual(null);
|
||||
|
||||
const update2 = yield* $(Queue.take(queue));
|
||||
const update2 = (await queue.next()).value;
|
||||
expect(update2.nested?.name).toEqual("nested");
|
||||
|
||||
map.nested!.name = "nestedUpdated";
|
||||
|
||||
const _ = yield* $(Queue.take(queue));
|
||||
const update3 = yield* $(Queue.take(queue));
|
||||
const _ = (await queue.next()).value;
|
||||
const update3 = (await queue.next()).value;
|
||||
expect(update3.nested?.name).toEqual("nestedUpdated");
|
||||
|
||||
const oldTwiceNested = update3.nested!.twiceNested;
|
||||
@@ -406,28 +423,26 @@ describe("CoMap resolution", async () => {
|
||||
|
||||
update3.nested = newNested;
|
||||
|
||||
yield* $(Queue.take(queue));
|
||||
// const update4 = yield* $(Queue.take(queue));
|
||||
const update4b = yield* $(Queue.take(queue));
|
||||
(await queue.next()).value;
|
||||
// const update4 = (await queue.next()).value;
|
||||
const update4b = (await queue.next()).value;
|
||||
|
||||
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 = yield* $(Queue.take(queue));
|
||||
const update5 = (await queue.next()).value;
|
||||
expect(update5.nested?.twiceNested?.taste).toEqual("salty");
|
||||
|
||||
newTwiceNested.taste = "umami";
|
||||
const update6 = yield* $(Queue.take(queue));
|
||||
const update6 = (await queue.next()).value;
|
||||
expect(update6.nested?.twiceNested?.taste).toEqual("umami");
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
class TestMapWithOptionalRef extends CoMap {
|
||||
color = co.string;
|
||||
nested = co.ref(NestedMap, { optional: true });
|
||||
nested = co.optional.ref(NestedMap);
|
||||
}
|
||||
|
||||
test("Construction with optional", async () => {
|
||||
@@ -559,3 +574,163 @@ describe("CoMap resolution", async () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CoMap applyDiff", async () => {
|
||||
const me = await Account.create({
|
||||
creationProps: { name: "Tester McTesterson" },
|
||||
crypto: Crypto,
|
||||
});
|
||||
|
||||
class TestMap extends CoMap {
|
||||
name = co.string;
|
||||
age = co.number;
|
||||
isActive = co.boolean;
|
||||
birthday = co.encoded(Encoders.Date);
|
||||
nested = co.ref(NestedMap);
|
||||
optionalField = co.optional.string;
|
||||
}
|
||||
|
||||
class NestedMap extends CoMap {
|
||||
value = co.string;
|
||||
}
|
||||
|
||||
test("Basic applyDiff", () => {
|
||||
const map = TestMap.create(
|
||||
{
|
||||
name: "Alice",
|
||||
age: 30,
|
||||
isActive: true,
|
||||
birthday: new Date("1990-01-01"),
|
||||
nested: NestedMap.create({ value: "original" }, { owner: me }),
|
||||
},
|
||||
{ owner: me },
|
||||
);
|
||||
|
||||
const newValues = {
|
||||
name: "Bob",
|
||||
age: 35,
|
||||
isActive: false,
|
||||
};
|
||||
|
||||
map.applyDiff(newValues);
|
||||
|
||||
expect(map.name).toEqual("Bob");
|
||||
expect(map.age).toEqual(35);
|
||||
expect(map.isActive).toEqual(false);
|
||||
expect(map.birthday).toEqual(new Date("1990-01-01"));
|
||||
expect(map.nested?.value).toEqual("original");
|
||||
});
|
||||
|
||||
test("applyDiff with nested changes", () => {
|
||||
const map = TestMap.create(
|
||||
{
|
||||
name: "Charlie",
|
||||
age: 25,
|
||||
isActive: true,
|
||||
birthday: new Date("1995-01-01"),
|
||||
nested: NestedMap.create({ value: "original" }, { owner: me }),
|
||||
},
|
||||
{ owner: me },
|
||||
);
|
||||
|
||||
const newValues = {
|
||||
name: "David",
|
||||
nested: NestedMap.create({ value: "updated" }, { owner: me }),
|
||||
};
|
||||
|
||||
map.applyDiff(newValues);
|
||||
|
||||
expect(map.name).toEqual("David");
|
||||
expect(map.age).toEqual(25);
|
||||
expect(map.nested?.value).toEqual("updated");
|
||||
});
|
||||
|
||||
test("applyDiff with encoded fields", () => {
|
||||
const map = TestMap.create(
|
||||
{
|
||||
name: "Eve",
|
||||
age: 28,
|
||||
isActive: true,
|
||||
birthday: new Date("1993-01-01"),
|
||||
nested: NestedMap.create({ value: "original" }, { owner: me }),
|
||||
},
|
||||
{ owner: me },
|
||||
);
|
||||
|
||||
const newValues = {
|
||||
birthday: new Date("1993-06-15"),
|
||||
};
|
||||
|
||||
map.applyDiff(newValues);
|
||||
|
||||
expect(map.birthday).toEqual(new Date("1993-06-15"));
|
||||
});
|
||||
|
||||
test("applyDiff with optional fields", () => {
|
||||
const map = TestMap.create(
|
||||
{
|
||||
name: "Frank",
|
||||
age: 40,
|
||||
isActive: true,
|
||||
birthday: new Date("1980-01-01"),
|
||||
nested: NestedMap.create({ value: "original" }, { owner: me }),
|
||||
},
|
||||
{ owner: me },
|
||||
);
|
||||
|
||||
const newValues = {
|
||||
optionalField: "New optional value",
|
||||
};
|
||||
|
||||
map.applyDiff(newValues);
|
||||
|
||||
expect(map.optionalField).toEqual("New optional value");
|
||||
|
||||
map.applyDiff({ optionalField: undefined });
|
||||
|
||||
expect(map.optionalField).toBeUndefined();
|
||||
});
|
||||
|
||||
test("applyDiff with no changes", () => {
|
||||
const map = TestMap.create(
|
||||
{
|
||||
name: "Grace",
|
||||
age: 35,
|
||||
isActive: true,
|
||||
birthday: new Date("1985-01-01"),
|
||||
nested: NestedMap.create({ value: "original" }, { owner: me }),
|
||||
},
|
||||
{ owner: me },
|
||||
);
|
||||
|
||||
const originalJSON = map.toJSON();
|
||||
|
||||
map.applyDiff({});
|
||||
|
||||
expect(map.toJSON()).toEqual(originalJSON);
|
||||
});
|
||||
|
||||
test("applyDiff with invalid field", () => {
|
||||
const map = TestMap.create(
|
||||
{
|
||||
name: "Henry",
|
||||
age: 45,
|
||||
isActive: false,
|
||||
birthday: new Date("1975-01-01"),
|
||||
nested: NestedMap.create({ value: "original" }, { owner: me }),
|
||||
},
|
||||
{ owner: me },
|
||||
);
|
||||
|
||||
const newValues = {
|
||||
name: "Ian",
|
||||
invalidField: "This should be ignored",
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
map.applyDiff(newValues as any);
|
||||
|
||||
expect(map.name).toEqual("Ian");
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect((map as any).invalidField).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
co,
|
||||
WasmCrypto,
|
||||
isControlledAccount,
|
||||
cojsonInternals,
|
||||
} from "../index.js";
|
||||
|
||||
const Crypto = await WasmCrypto.create();
|
||||
@@ -83,11 +83,13 @@ describe("CoStream resolution", async () => {
|
||||
|
||||
test("Loading and availability", async () => {
|
||||
const { me, stream } = await initNodeAndStream();
|
||||
const [initialAsPeer, secondPeer] = await Effect.runPromise(
|
||||
connectedPeers("initial", "second", {
|
||||
const [initialAsPeer, secondPeer] = connectedPeers(
|
||||
"initial",
|
||||
"second",
|
||||
{
|
||||
peer1role: "server",
|
||||
peer2role: "client",
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (!isControlledAccount(me)) {
|
||||
throw "me is not a controlled account";
|
||||
@@ -176,12 +178,15 @@ describe("CoStream resolution", async () => {
|
||||
test("Subscription & auto-resolution", async () => {
|
||||
const { me, stream } = await initNodeAndStream();
|
||||
|
||||
const [initialAsPeer, secondAsPeer] = await Effect.runPromise(
|
||||
connectedPeers("initial", "second", {
|
||||
const [initialAsPeer, secondAsPeer] = connectedPeers(
|
||||
"initial",
|
||||
"second",
|
||||
{
|
||||
peer1role: "server",
|
||||
peer2role: "client",
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
me._raw.core.node.syncManager.addPeer(secondAsPeer);
|
||||
if (!isControlledAccount(me)) {
|
||||
throw "me is not a controlled account";
|
||||
@@ -195,78 +200,68 @@ describe("CoStream resolution", async () => {
|
||||
crypto: Crypto,
|
||||
});
|
||||
|
||||
await Effect.runPromise(
|
||||
Effect.gen(function* ($) {
|
||||
const queue = yield* $(Queue.unbounded<TestStream>());
|
||||
const queue = new cojsonInternals.Channel();
|
||||
|
||||
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),
|
||||
);
|
||||
},
|
||||
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 queue.push(subscribedStream);
|
||||
},
|
||||
);
|
||||
|
||||
const update1 = yield* $(Queue.take(queue));
|
||||
expect(update1[me.id]?.value).toEqual(null);
|
||||
const update1 = (await queue.next()).value;
|
||||
expect(update1[me.id]?.value).toEqual(null);
|
||||
|
||||
const update2 = yield* $(Queue.take(queue));
|
||||
expect(update2[me.id]?.value).toBeDefined();
|
||||
expect(update2[me.id]?.value?.[me.id]?.value).toBe(null);
|
||||
const update2 = (await queue.next()).value;
|
||||
expect(update2[me.id]?.value).toBeDefined();
|
||||
expect(update2[me.id]?.value?.[me.id]?.value).toBe(null);
|
||||
|
||||
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");
|
||||
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",
|
||||
);
|
||||
|
||||
update3[me.id]!.value![me.id]!.value!.push("bread");
|
||||
update3[me.id]!.value![me.id]!.value!.push("bread");
|
||||
|
||||
const update4 = yield* $(Queue.take(queue));
|
||||
expect(
|
||||
update4[me.id]?.value?.[me.id]?.value?.[me.id]?.value,
|
||||
).toBe("bread");
|
||||
const update4 = (await queue.next()).value;
|
||||
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 = yield* $(Queue.take(queue));
|
||||
expect(
|
||||
update5[me.id]?.value?.[me.id]?.value?.[me.id]?.value,
|
||||
).toBe("butter");
|
||||
const update5 = (await queue.next()).value;
|
||||
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 = yield* $(Queue.take(queue));
|
||||
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 = (await queue.next()).value;
|
||||
expect(update6[me.id]?.value?.[me.id]?.value?.[me.id]?.value).toBe(
|
||||
"jam",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -327,11 +322,13 @@ describe("BinaryCoStream loading & Subscription", async () => {
|
||||
|
||||
test("Loading and availability", async () => {
|
||||
const { me, stream } = await initNodeAndStream();
|
||||
const [initialAsPeer, secondAsPeer] = await Effect.runPromise(
|
||||
connectedPeers("initial", "second", {
|
||||
const [initialAsPeer, secondAsPeer] = connectedPeers(
|
||||
"initial",
|
||||
"second",
|
||||
{
|
||||
peer1role: "server",
|
||||
peer2role: "client",
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (!isControlledAccount(me)) {
|
||||
throw "me is not a controlled account";
|
||||
@@ -360,98 +357,83 @@ describe("BinaryCoStream loading & Subscription", async () => {
|
||||
});
|
||||
|
||||
test("Subscription", async () => {
|
||||
await Effect.runPromise(
|
||||
Effect.gen(function* ($) {
|
||||
const { me } = yield* Effect.promise(() => initNodeAndStream());
|
||||
const { me } = await initNodeAndStream();
|
||||
const stream = BinaryCoStream.create({ owner: me });
|
||||
|
||||
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,
|
||||
});
|
||||
}),
|
||||
const [initialAsPeer, secondAsPeer] = 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 = 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
ID,
|
||||
} from "../index.js";
|
||||
import { newRandomSessionID } from "cojson/src/coValueCore.js";
|
||||
import { Effect } from "effect";
|
||||
|
||||
class TestMap extends CoMap {
|
||||
list = co.ref(TestList);
|
||||
@@ -39,10 +38,11 @@ describe("Deep loading with depth arg", async () => {
|
||||
crypto: Crypto,
|
||||
});
|
||||
|
||||
const [initialAsPeer, secondPeer] = await Effect.runPromise(connectedPeers("initial", "second", {
|
||||
const [initialAsPeer, secondPeer] = connectedPeers("initial", "second", {
|
||||
peer1role: "server",
|
||||
peer2role: "client",
|
||||
}));
|
||||
});
|
||||
|
||||
if (!isControlledAccount(me)) {
|
||||
throw "me is not a controlled account";
|
||||
}
|
||||
@@ -138,8 +138,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]?.value).toBe(null);
|
||||
expect(map4.list[0]?.stream?.byMe?.value).toBe(null);
|
||||
expect(map4.list[0]?.stream?.[me.id]).toBe(undefined)
|
||||
expect(map4.list[0]?.stream?.byMe?.value).toBe(undefined);
|
||||
|
||||
const map5 = await TestMap.load(map.id, meOnSecondPeer, {
|
||||
list: [{ stream: [{}] }],
|
||||
@@ -252,13 +252,15 @@ test("Deep loading a record-like coMap", async () => {
|
||||
crypto: Crypto,
|
||||
});
|
||||
|
||||
const [initialAsPeer, secondPeer] = await Effect.runPromise(connectedPeers("initial", "second", {
|
||||
const [initialAsPeer, secondPeer] = 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,
|
||||
|
||||
244
pnpm-lock.yaml
generated
244
pnpm-lock.yaml
generated
@@ -11,12 +11,6 @@ importers:
|
||||
'@changesets/cli':
|
||||
specifier: ^2.27.3
|
||||
version: 2.27.3
|
||||
husky:
|
||||
specifier: ^9.0.11
|
||||
version: 9.0.11
|
||||
lint-staged:
|
||||
specifier: ^15.2.2
|
||||
version: 15.2.2
|
||||
prettier:
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.1
|
||||
@@ -162,9 +156,6 @@ 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
|
||||
@@ -450,12 +441,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
|
||||
queueable:
|
||||
specifier: ^5.3.2
|
||||
version: 5.3.2
|
||||
devDependencies:
|
||||
'@types/jest':
|
||||
specifier: ^29.5.3
|
||||
@@ -487,9 +478,6 @@ 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
|
||||
@@ -512,9 +500,6 @@ 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
|
||||
@@ -528,9 +513,6 @@ 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
|
||||
@@ -565,9 +547,6 @@ 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
|
||||
@@ -608,9 +587,6 @@ 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
|
||||
@@ -695,15 +671,9 @@ 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
|
||||
@@ -1947,10 +1917,6 @@ packages:
|
||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
ansi-escapes@6.2.1:
|
||||
resolution: {integrity: sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2200,14 +2166,6 @@ packages:
|
||||
class-variance-authority@0.7.0:
|
||||
resolution: {integrity: sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==}
|
||||
|
||||
cli-cursor@4.0.0:
|
||||
resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
cli-truncate@4.0.0:
|
||||
resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
cliui@6.0.0:
|
||||
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||
|
||||
@@ -2236,13 +2194,6 @@ packages:
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
colorette@2.0.20:
|
||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||
|
||||
commander@11.1.0:
|
||||
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
commander@4.1.1:
|
||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -2467,9 +2418,6 @@ packages:
|
||||
electron-to-chromium@1.4.615:
|
||||
resolution: {integrity: sha512-/bKPPcgZVUziECqDc+0HkT87+0zhaWSZHNXqF8FLd2lQcptpmUFwoCSWjCdOng9Gdq+afKArPdEg/0ZW461Eng==}
|
||||
|
||||
emoji-regex@10.3.0:
|
||||
resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
@@ -2617,9 +2565,6 @@ packages:
|
||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
eventemitter3@5.0.1:
|
||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||
|
||||
execa@8.0.1:
|
||||
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
||||
engines: {node: '>=16.17'}
|
||||
@@ -2670,6 +2615,9 @@ 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==}
|
||||
|
||||
@@ -2784,10 +2732,6 @@ packages:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
|
||||
get-east-asian-width@1.2.0:
|
||||
resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
get-func-name@2.0.2:
|
||||
resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
|
||||
|
||||
@@ -2942,11 +2886,6 @@ packages:
|
||||
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
||||
engines: {node: '>=16.17.0'}
|
||||
|
||||
husky@9.0.11:
|
||||
resolution: {integrity: sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
hyphenate-style-name@1.0.4:
|
||||
resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==}
|
||||
|
||||
@@ -3046,14 +2985,6 @@ packages:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-fullwidth-code-point@4.0.0:
|
||||
resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
is-fullwidth-code-point@5.0.0:
|
||||
resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
is-glob@4.0.3:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3227,18 +3158,9 @@ packages:
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
lint-staged@15.2.2:
|
||||
resolution: {integrity: sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
hasBin: true
|
||||
|
||||
listenercount@1.0.1:
|
||||
resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==}
|
||||
|
||||
listr2@8.0.1:
|
||||
resolution: {integrity: sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
load-yaml-file@0.2.0:
|
||||
resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -3277,10 +3199,6 @@ packages:
|
||||
lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
|
||||
log-update@6.0.0:
|
||||
resolution: {integrity: sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
loglevel-plugin-prefix@0.8.4:
|
||||
resolution: {integrity: sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==}
|
||||
|
||||
@@ -3365,10 +3283,6 @@ packages:
|
||||
engines: {node: '>=10.0.0'}
|
||||
hasBin: true
|
||||
|
||||
mimic-fn@2.1.0:
|
||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
mimic-fn@4.0.0:
|
||||
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -3539,10 +3453,6 @@ packages:
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
onetime@5.1.2:
|
||||
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
onetime@6.0.0:
|
||||
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -3663,11 +3573,6 @@ packages:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
pidtree@0.6.0:
|
||||
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
|
||||
engines: {node: '>=0.10'}
|
||||
hasBin: true
|
||||
|
||||
pify@2.3.0:
|
||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3817,6 +3722,10 @@ 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'}
|
||||
@@ -3938,17 +3847,10 @@ packages:
|
||||
resq@1.11.0:
|
||||
resolution: {integrity: sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==}
|
||||
|
||||
restore-cursor@4.0.0:
|
||||
resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
reusify@1.0.4:
|
||||
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
|
||||
rfdc@1.3.1:
|
||||
resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==}
|
||||
|
||||
rgb2hex@0.2.5:
|
||||
resolution: {integrity: sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==}
|
||||
|
||||
@@ -4081,14 +3983,6 @@ packages:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
slice-ansi@5.0.0:
|
||||
resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
slice-ansi@7.1.0:
|
||||
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
smart-buffer@4.2.0:
|
||||
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
|
||||
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
|
||||
@@ -4168,10 +4062,6 @@ packages:
|
||||
streamx@2.15.6:
|
||||
resolution: {integrity: sha512-q+vQL4AAz+FdfT137VF69Cc/APqUbxy+MDOImRrMvchJpigHj9GksgDU2LYbO9rx7RX6osWgxJB2WxhYv4SZAw==}
|
||||
|
||||
string-argv@0.3.2:
|
||||
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
|
||||
engines: {node: '>=0.6.19'}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4180,10 +4070,6 @@ packages:
|
||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
string-width@7.1.0:
|
||||
resolution: {integrity: sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
string.prototype.trim@1.2.9:
|
||||
resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4736,10 +4622,6 @@ packages:
|
||||
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
wrap-ansi@9.0.0:
|
||||
resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
@@ -6168,8 +6050,6 @@ snapshots:
|
||||
|
||||
ansi-colors@4.1.3: {}
|
||||
|
||||
ansi-escapes@6.2.1: {}
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.0.1: {}
|
||||
@@ -6444,15 +6324,6 @@ snapshots:
|
||||
dependencies:
|
||||
clsx: 2.0.0
|
||||
|
||||
cli-cursor@4.0.0:
|
||||
dependencies:
|
||||
restore-cursor: 4.0.0
|
||||
|
||||
cli-truncate@4.0.0:
|
||||
dependencies:
|
||||
slice-ansi: 5.0.0
|
||||
string-width: 7.1.0
|
||||
|
||||
cliui@6.0.0:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
@@ -6481,10 +6352,6 @@ snapshots:
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
colorette@2.0.20: {}
|
||||
|
||||
commander@11.1.0: {}
|
||||
|
||||
commander@4.1.1: {}
|
||||
|
||||
commander@9.5.0: {}
|
||||
@@ -6688,8 +6555,6 @@ snapshots:
|
||||
|
||||
electron-to-chromium@1.4.615: {}
|
||||
|
||||
emoji-regex@10.3.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
@@ -6947,8 +6812,6 @@ snapshots:
|
||||
|
||||
esutils@2.0.3: {}
|
||||
|
||||
eventemitter3@5.0.1: {}
|
||||
|
||||
execa@8.0.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.3
|
||||
@@ -7013,6 +6876,8 @@ snapshots:
|
||||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fast-list@1.0.3: {}
|
||||
|
||||
fast-loops@1.1.3: {}
|
||||
|
||||
fast-querystring@1.1.2:
|
||||
@@ -7142,8 +7007,6 @@ snapshots:
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
||||
get-east-asian-width@1.2.0: {}
|
||||
|
||||
get-func-name@2.0.2: {}
|
||||
|
||||
get-intrinsic@1.2.4:
|
||||
@@ -7323,8 +7186,6 @@ snapshots:
|
||||
|
||||
human-signals@5.0.0: {}
|
||||
|
||||
husky@9.0.11: {}
|
||||
|
||||
hyphenate-style-name@1.0.4: {}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
@@ -7414,12 +7275,6 @@ snapshots:
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
is-fullwidth-code-point@4.0.0: {}
|
||||
|
||||
is-fullwidth-code-point@5.0.0:
|
||||
dependencies:
|
||||
get-east-asian-width: 1.2.0
|
||||
|
||||
is-glob@4.0.3:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
@@ -7578,32 +7433,8 @@ snapshots:
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
lint-staged@15.2.2:
|
||||
dependencies:
|
||||
chalk: 5.3.0
|
||||
commander: 11.1.0
|
||||
debug: 4.3.4
|
||||
execa: 8.0.1
|
||||
lilconfig: 3.0.0
|
||||
listr2: 8.0.1
|
||||
micromatch: 4.0.5
|
||||
pidtree: 0.6.0
|
||||
string-argv: 0.3.2
|
||||
yaml: 2.3.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
listenercount@1.0.1: {}
|
||||
|
||||
listr2@8.0.1:
|
||||
dependencies:
|
||||
cli-truncate: 4.0.0
|
||||
colorette: 2.0.20
|
||||
eventemitter3: 5.0.1
|
||||
log-update: 6.0.0
|
||||
rfdc: 1.3.1
|
||||
wrap-ansi: 9.0.0
|
||||
|
||||
load-yaml-file@0.2.0:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -7642,14 +7473,6 @@ snapshots:
|
||||
|
||||
lodash@4.17.21: {}
|
||||
|
||||
log-update@6.0.0:
|
||||
dependencies:
|
||||
ansi-escapes: 6.2.1
|
||||
cli-cursor: 4.0.0
|
||||
slice-ansi: 7.1.0
|
||||
strip-ansi: 7.1.0
|
||||
wrap-ansi: 9.0.0
|
||||
|
||||
loglevel-plugin-prefix@0.8.4: {}
|
||||
|
||||
loglevel@1.8.1: {}
|
||||
@@ -7722,8 +7545,6 @@ snapshots:
|
||||
|
||||
mime@3.0.0: {}
|
||||
|
||||
mimic-fn@2.1.0: {}
|
||||
|
||||
mimic-fn@4.0.0: {}
|
||||
|
||||
mimic-response@3.1.0: {}
|
||||
@@ -7876,10 +7697,6 @@ snapshots:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
onetime@5.1.2:
|
||||
dependencies:
|
||||
mimic-fn: 2.1.0
|
||||
|
||||
onetime@6.0.0:
|
||||
dependencies:
|
||||
mimic-fn: 4.0.0
|
||||
@@ -7997,8 +7814,6 @@ snapshots:
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
pidtree@0.6.0: {}
|
||||
|
||||
pify@2.3.0: {}
|
||||
|
||||
pify@4.0.1: {}
|
||||
@@ -8180,6 +7995,10 @@ 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: {}
|
||||
@@ -8327,15 +8146,8 @@ snapshots:
|
||||
dependencies:
|
||||
fast-deep-equal: 2.0.1
|
||||
|
||||
restore-cursor@4.0.0:
|
||||
dependencies:
|
||||
onetime: 5.1.2
|
||||
signal-exit: 3.0.7
|
||||
|
||||
reusify@1.0.4: {}
|
||||
|
||||
rfdc@1.3.1: {}
|
||||
|
||||
rgb2hex@0.2.5: {}
|
||||
|
||||
rimraf@2.7.1:
|
||||
@@ -8482,16 +8294,6 @@ snapshots:
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
slice-ansi@5.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.1
|
||||
is-fullwidth-code-point: 4.0.0
|
||||
|
||||
slice-ansi@7.1.0:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.1
|
||||
is-fullwidth-code-point: 5.0.0
|
||||
|
||||
smart-buffer@4.2.0: {}
|
||||
|
||||
smartwrap@2.0.2:
|
||||
@@ -8579,8 +8381,6 @@ snapshots:
|
||||
fast-fifo: 1.3.2
|
||||
queue-tick: 1.0.1
|
||||
|
||||
string-argv@0.3.2: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
@@ -8593,12 +8393,6 @@ snapshots:
|
||||
emoji-regex: 9.2.2
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
string-width@7.1.0:
|
||||
dependencies:
|
||||
emoji-regex: 10.3.0
|
||||
get-east-asian-width: 1.2.0
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
string.prototype.trim@1.2.9:
|
||||
dependencies:
|
||||
call-bind: 1.0.7
|
||||
@@ -9307,12 +9101,6 @@ snapshots:
|
||||
string-width: 5.1.2
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
wrap-ansi@9.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.1
|
||||
string-width: 7.1.0
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
ws@8.13.0: {}
|
||||
|
||||
Reference in New Issue
Block a user