Compare commits

..

51 Commits

Author SHA1 Message Date
Anselm
55d1e7b03a More progress 2024-07-31 10:37:10 +01:00
Anselm
0f37a2ba69 Merge branch 'main' into rich-text 2024-07-29 11:23:52 +01:00
Anselm
b09e35e372 release 2024-07-29 10:40:10 +01:00
Anselm
d2c8121c9c Fix storage option in jazz-react 2024-07-29 10:34:39 +01:00
Anselm
380bb88ffa Mostly complete OPFS implementation (single-tab only) 2024-07-29 10:33:18 +01:00
Anselm
6ab53c263d Release 2024-07-26 17:23:02 +01:00
Anselm
e7f3e4e242 Increase disconnect timeout for now 2024-07-26 17:21:06 +01:00
Anselm Eickhoff
8bb5201647 Merge pull request #236 from timolins/patch-1
[Homepage] Make current year dynamic in footer
2024-07-22 15:55:16 +01:00
Timo Lins
a9fc94f53d Make current year dynamic in footer 2024-07-22 10:06:48 +02:00
Anselm Eickhoff
ca7c0510d1 Merge pull request #234 from Schniz/schniz/co-optional 2024-07-21 10:14:41 +01:00
Gal Schlezinger
1bf16f0859 add co.optional syntax 2024-07-21 09:10:44 +03:00
Anselm Eickhoff
21b503c188 Merge pull request #224 from datner/datner/give-it-a-try
cojson-transport-ws: reuse runtime and use fibers instead of setTimeout
2024-07-15 14:35:18 +01:00
Anselm
0053e9796c Release 2024-07-15 11:01:31 +01:00
Anselm
e84941b1b1 Fix another bug in CoMap 'has' proxy trap 2024-07-15 10:59:14 +01:00
Anselm
57f6f8d67e Release 2024-07-14 17:55:47 +01:00
Anselm
5b8e69d973 Fix bug in CoMap 'has' trap 2024-07-14 17:55:05 +01:00
Anselm
7213b1bfa3 Release 2024-07-13 13:32:50 +01:00
Anselm Eickhoff
11f0770f08 Merge pull request #230 from tobiaslins/add-support-for-in
Add support for property existence
2024-07-13 13:30:15 +01:00
Tobias Lins
44e6dc3ae8 Remove useless check 2024-07-13 13:57:37 +02:00
Tobias Lins
b5d20d2488 Better implementation 2024-07-13 13:26:28 +02:00
Tobias Lins
0185545838 Add support for property existence 2024-07-13 12:26:10 +02:00
Yuval Datner
8c8f85859c style: prettier 2024-07-12 15:42:52 +03:00
Yuval Datner
104384409e refactor: change to yieldable error 2024-07-12 15:42:27 +03:00
Yuval Datner
179827ae56 small refactor for readability 2024-07-12 15:13:25 +03:00
Yuval Datner
6645829876 do stream stuff 2024-07-12 15:13:23 +03:00
Gal Schlezinger
68cb302722 store jazzPings on global 2024-07-12 15:13:22 +03:00
Gal Schlezinger
8dc33f2790 fix bugs because I misindented things 2024-07-12 15:13:21 +03:00
Gal Schlezinger
5f64ba326c jazzPings 2024-07-12 15:13:20 +03:00
Gal Schlezinger
7ccb15107c cojson-transport-ws: reuse runtime and use fibers instead of setTimeout
when calling Effect.runFork we don't propagate layers
and using fibers can allow us to interrupt ongoing requests
when the pings fail
2024-07-12 15:13:16 +03:00
Anselm
b102964743 Get rid of husky for now 2024-07-12 11:56:54 +01:00
Anselm
216d50a09c Remove old homepage build 2024-07-12 11:55:44 +01:00
Anselm
07ea59fdcb Release 2024-07-12 11:14:15 +01:00
Anselm
932a84a47f Update to Effect 3.5.2 2024-07-12 11:13:41 +01:00
Anselm
34dda7bdbd Release 2024-07-05 11:18:53 +01:00
Anselm
49fa153581 Merge branch 'fix-fs' 2024-07-05 11:17:54 +01:00
Anselm
c80b827775 Release 2024-06-30 16:28:53 +01:00
Anselm Eickhoff
a2bf9f988a Merge pull request #218 from Schniz/fix-mutating-nullable-field 2024-06-30 08:31:57 +01:00
Gal Schlezinger
ac27b2d5c2 jazz-tools: allow to mutate nullable fields into null
when having a co.encoded(Schema.NullOr(Schema.String)), construction
with null works well, but mutating a value into null throws.
This commit fixes it and adds a test that verifies it actually works.
2024-06-30 09:49:15 +03:00
Anselm
c813518fdc Release 2024-06-28 16:27:15 +01:00
Anselm
d5034ed5c3 Provide current res in ProgressiveImg 2024-06-28 16:26:03 +01:00
Anselm
cf2c29a365 Use verce lanalytics & speed insights 2024-06-28 14:54:58 +01:00
Anselm
d948823db6 Add package manager to package.json 2024-06-28 12:08:23 +01:00
Anselm
060ad4630d Resurrect inspector 2024-06-27 16:54:23 +01:00
Anselm
00019c7578 More plaintext progress 2024-06-06 11:47:17 +01:00
Anselm
cdb0ac2b9f Rich text progress 2024-06-04 17:54:28 +01:00
Anselm
a1d84c433c Merge branch 'main' into rich-text 2024-06-04 13:32:18 +01:00
Anselm
40eb81135c Progress on text 2024-06-03 16:48:18 +01:00
Anselm
65fad3d84c Merge branch 'main' into rich-text 2024-06-01 19:36:27 +02:00
Anselm
00cad168c9 More implementation 2024-05-06 11:59:14 +01:00
Anselm
ce5475f127 Merge branch 'jazz-schema' into rich-text 2024-05-03 16:36:15 +01:00
Anselm
3095caaa6d Implement RawCoPlainText 2024-05-03 14:27:35 +01:00
106 changed files with 7574 additions and 3336 deletions

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
example: ["chat", "pets", "todo"]
example: ["chat", "pets", "todo", "inspector"]
# example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop", "inspector"]
steps:
@@ -53,59 +53,12 @@ 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
strategy:
matrix:
example: ["chat", "pets", "todo"]
example: ["chat", "pets", "todo", "inspector"]
# example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop", "inspector"]
steps:
@@ -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 }}

View File

@@ -1,2 +0,0 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"

View File

@@ -1,5 +1,77 @@
# jazz-example-chat
## 0.0.70
### Patch Changes
- Updated dependencies
- cojson@0.7.23
- jazz-react@0.7.23
- jazz-tools@0.7.23
## 0.0.69
### Patch Changes
- jazz-react@0.7.22
## 0.0.68
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.21
- jazz-react@0.7.21
## 0.0.67
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.20
- jazz-react@0.7.20
## 0.0.66
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.19
- jazz-react@0.7.19
## 0.0.65
### Patch Changes
- Updated dependencies
- cojson@0.7.18
- jazz-react@0.7.18
- jazz-tools@0.7.18
## 0.0.64
### Patch Changes
- Updated dependencies
- cojson@0.7.17
- jazz-react@0.7.17
- jazz-tools@0.7.17
## 0.0.63
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.16
- jazz-react@0.7.16
## 0.0.62
### Patch Changes
- Updated dependencies
- jazz-react@0.7.15
## 0.0.61
### Patch Changes

View File

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

View File

@@ -0,0 +1,49 @@
# jazz-example-chat
## 0.0.51
### Patch Changes
- Updated dependencies
- cojson@0.7.23
- cojson-transport-ws@0.7.23
## 0.0.50
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.22
## 0.0.49
### Patch Changes
- Updated dependencies
- cojson@0.7.18
- cojson-transport-ws@0.7.18
## 0.0.48
### Patch Changes
- Updated dependencies
- cojson@0.7.17
- cojson-transport-ws@0.7.17
## 0.0.47
### Patch Changes
- Updated dependencies
- cojson@0.6.7
- jazz-react@0.5.5
- jazz-react-auth-local@0.4.18
## 0.0.46
### Patch Changes
- Updated dependencies
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16

View File

@@ -1,7 +1,7 @@
{
"name": "inspector",
"name": "jazz-inspector",
"private": true,
"version": "0.0.47",
"version": "0.0.51",
"type": "module",
"scripts": {
"dev": "vite",
@@ -17,9 +17,9 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"hash-slash": "workspace:*",
"jazz-react": "workspace:*",
"jazz-react-auth-local": "workspace:*",
"cojson": "workspace:*",
"cojson-transport-ws": "workspace:*",
"effect": "^3.5.2",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -0,0 +1,309 @@
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";
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>
);
}

View File

@@ -0,0 +1,249 @@
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>
);
}

View File

@@ -1,5 +1,80 @@
# jazz-example-pets
## 0.0.88
### Patch Changes
- Updated dependencies
- jazz-react@0.7.23
- jazz-tools@0.7.23
- jazz-browser-media-images@0.7.23
## 0.0.87
### Patch Changes
- jazz-browser-media-images@0.7.22
- jazz-react@0.7.22
## 0.0.86
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.21
- jazz-browser-media-images@0.7.21
- jazz-react@0.7.21
## 0.0.85
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.20
- jazz-browser-media-images@0.7.20
- jazz-react@0.7.20
## 0.0.84
### 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
- jazz-react@0.7.18
- jazz-tools@0.7.18
- jazz-browser-media-images@0.7.18
## 0.0.82
### Patch Changes
- jazz-react@0.7.17
- jazz-tools@0.7.17
- jazz-browser-media-images@0.7.17
## 0.0.81
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.16
- jazz-browser-media-images@0.7.16
- jazz-react@0.7.16
## 0.0.80
### Patch Changes
- Updated dependencies
- jazz-react@0.7.15
## 0.0.79
### Patch Changes

View File

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

View File

@@ -0,0 +1,13 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {},
}

24
examples/richtext/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,528 @@
# jazz-example-chat
## 0.0.57
### Patch Changes
- Updated dependencies
- cojson@0.7.10
- jazz-react@0.7.10
- jazz-tools@0.7.10
## 0.0.56
### Patch Changes
- Updated dependencies
- cojson@0.7.9
- jazz-react@0.7.9
- jazz-tools@0.7.9
## 0.0.55
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.8
- jazz-react@0.7.8
## 0.0.54
### Patch Changes
- Updated dependencies [9fdc91c]
- jazz-react@0.7.7
## 0.0.53
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.6
- jazz-react@0.7.6
## 0.0.52
### Patch Changes
- Updated dependencies
- jazz-react@0.7.5
## 0.0.51
### Patch Changes
- Updated dependencies
- jazz-react@0.7.4
## 0.0.50
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.3
- jazz-react@0.7.3
## 0.0.49
### Patch Changes
- Updated dependencies
- jazz-react@0.7.2
## 0.0.48
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.1
- jazz-react@0.7.1
## 0.0.47
### Patch Changes
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [96c494f]
- Updated dependencies [59c18c3]
- Updated dependencies [19f52b7]
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [d8fe2b1]
- Updated dependencies [19004b4]
- Updated dependencies [a78f168]
- Updated dependencies [1200aae]
- Updated dependencies [60d5ca2]
- Updated dependencies [52675c9]
- Updated dependencies [129e2c1]
- Updated dependencies [6d49e9b]
- Updated dependencies [1cfa279]
- Updated dependencies [704af7d]
- Updated dependencies [e97f730]
- Updated dependencies [1a35307]
- Updated dependencies [460478f]
- Updated dependencies [6b0418f]
- Updated dependencies [e299c3e]
- Updated dependencies [ed5643a]
- Updated dependencies [bde684f]
- Updated dependencies [bf0f8ec]
- Updated dependencies [c4151fc]
- Updated dependencies [63374cc]
- Updated dependencies [8636319]
- Updated dependencies [01ac646]
- Updated dependencies [a5e68a4]
- Updated dependencies [8636319]
- Updated dependencies [952982e]
- Updated dependencies [1a35307]
- Updated dependencies [5fa277c]
- Updated dependencies [60d5ca2]
- Updated dependencies [21771c4]
- Updated dependencies [77c2b56]
- Updated dependencies [63374cc]
- Updated dependencies [d2e03ff]
- Updated dependencies [354bdcd]
- Updated dependencies [ece35b3]
- Updated dependencies [60d5ca2]
- Updated dependencies [69ac514]
- Updated dependencies [f8a5c46]
- Updated dependencies [f0f6f1b]
- Updated dependencies [e5eed5b]
- Updated dependencies [1a44f87]
- Updated dependencies [627d895]
- Updated dependencies [1200aae]
- Updated dependencies [63374cc]
- Updated dependencies [ece35b3]
- Updated dependencies [38d4410]
- Updated dependencies [85d2b62]
- Updated dependencies [fd86c11]
- Updated dependencies [52675c9]
- jazz-tools@0.7.0
- cojson@0.7.0
- jazz-react@0.7.0
- hash-slash@0.2.0
## 0.0.47-alpha.42
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.42
- cojson@0.7.0-alpha.42
- jazz-react@0.7.0-alpha.42
## 0.0.47-alpha.41
### Patch Changes
- jazz-tools@0.7.0-alpha.41
- jazz-react@0.7.0-alpha.41
## 0.0.47-alpha.40
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.40
## 0.0.47-alpha.39
### Patch Changes
- Updated dependencies
- cojson@0.7.0-alpha.39
- jazz-react@0.7.0-alpha.39
- jazz-tools@0.7.0-alpha.39
## 0.0.47-alpha.38
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.38
- jazz-react@0.7.0-alpha.38
- cojson@0.7.0-alpha.38
## 0.0.47-alpha.37
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.37
- cojson@0.7.0-alpha.37
- jazz-tools@0.7.0-alpha.37
## 0.0.47-alpha.36
### Patch Changes
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [6b0418f]
- Updated dependencies [1a35307]
- cojson@0.7.0-alpha.36
- jazz-tools@0.7.0-alpha.36
- jazz-react@0.7.0-alpha.36
## 0.0.47-alpha.35
### Patch Changes
- Updated dependencies
- Updated dependencies
- cojson@0.7.0-alpha.35
- jazz-tools@0.7.0-alpha.35
- jazz-react@0.7.0-alpha.35
## 0.0.47-alpha.34
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.34
- jazz-react@0.7.0-alpha.34
## 0.0.47-alpha.33
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.33
## 0.0.47-alpha.32
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- hash-slash@0.2.0-alpha.3
- jazz-tools@0.7.0-alpha.32
- jazz-react@0.7.0-alpha.32
## 0.0.47-alpha.31
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.31
- jazz-react@0.7.0-alpha.31
## 0.0.47-alpha.30
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.30
- jazz-react@0.7.0-alpha.30
## 0.0.47-alpha.29
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.29
- cojson@0.7.0-alpha.29
- jazz-react@0.7.0-alpha.29
## 0.0.47-alpha.28
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.28
- cojson@0.7.0-alpha.28
- jazz-react@0.7.0-alpha.28
## 0.0.47-alpha.27
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.27
- cojson@0.7.0-alpha.27
- jazz-react@0.7.0-alpha.27
## 0.0.47-alpha.26
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.26
- jazz-react@0.7.0-alpha.26
## 0.0.47-alpha.25
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.25
- jazz-react@0.7.0-alpha.25
## 0.0.47-alpha.24
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.24
- cojson@0.7.0-alpha.24
- jazz-react@0.7.0-alpha.24
## 0.0.47-alpha.23
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.23
- jazz-react@0.7.0-alpha.23
## 0.0.47-alpha.22
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.22
- jazz-react@0.7.0-alpha.22
## 0.0.47-alpha.21
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.21
- jazz-tools@0.7.0-alpha.21
## 0.0.47-alpha.20
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-react@0.7.0-alpha.20
- jazz-tools@0.7.0-alpha.20
## 0.0.47-alpha.19
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.19
- jazz-react@0.7.0-alpha.19
## 0.0.47-alpha.18
### Patch Changes
- jazz-react@0.7.0-alpha.18
## 0.0.47-alpha.17
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.17
- jazz-react@0.7.0-alpha.17
## 0.0.47-alpha.16
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.16
- jazz-react@0.7.0-alpha.16
## 0.0.47-alpha.15
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.15
- jazz-react@0.7.0-alpha.15
## 0.0.47-alpha.14
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.14
- jazz-react@0.7.0-alpha.14
## 0.0.47-alpha.13
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.13
- jazz-react@0.7.0-alpha.13
## 0.0.47-alpha.12
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.12
- jazz-tools@0.7.0-alpha.12
## 0.0.47-alpha.11
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.11
- jazz-tools@0.7.0-alpha.11
- cojson@0.7.0-alpha.11
## 0.0.47-alpha.10
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.10
- jazz-tools@0.7.0-alpha.10
- cojson@0.7.0-alpha.10
## 0.0.47-alpha.9
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.9
- jazz-tools@0.7.0-alpha.9
## 0.0.47-alpha.8
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.8
- jazz-tools@0.7.0-alpha.8
## 0.0.47-alpha.7
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.7
- jazz-tools@0.7.0-alpha.7
- cojson@0.7.0-alpha.7
## 0.0.47-alpha.6
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.6
- jazz-tools@0.7.0-alpha.6
## 0.0.47-alpha.5
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.5
- jazz-tools@0.7.0-alpha.5
- cojson@0.7.0-alpha.5
## 0.0.47-alpha.4
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.4
- jazz-react@0.7.0-alpha.4
## 0.0.47-alpha.3
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.3
- jazz-react@0.7.0-alpha.3
## 0.0.47-alpha.2
### Patch Changes
- Updated dependencies
- hash-slash@0.2.0-alpha.2
- jazz-react@0.7.0-alpha.2
- jazz-tools@0.7.0-alpha.2
## 0.0.47-alpha.1
### Patch Changes
- Updated dependencies
- hash-slash@0.2.0-alpha.1
- jazz-react@0.7.0-alpha.1
- jazz-tools@0.7.0-alpha.1
- cojson@0.7.0-alpha.1
## 0.0.47-alpha.0
### Patch Changes
- Updated dependencies
- hash-slash@0.2.0-alpha.0
- jazz-react@0.7.0-alpha.0
- jazz-tools@0.7.0-alpha.0
- cojson@0.7.0-alpha.0
## 0.0.46
### Patch Changes
- Updated dependencies
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16

View File

@@ -0,0 +1,4 @@
FROM caddy:2.7.3-alpine
LABEL org.opencontainers.image.source="https://github.com/gardencmp/jazz"
COPY ./dist /usr/share/caddy/

View File

@@ -0,0 +1,42 @@
# Jazz Chat Example
Live version: https://example-chat.jazz.tools
## Installing & running the example locally
(this requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation))
Start by checking out `jazz`
```bash
git clone https://github.com/gardencmp/jazz.git
cd jazz/examples/chat
pnpm pack --pack-destination /tmp
mkdir -p ~/jazz-examples/chat # or any other directory
tar -xf /tmp/jazz-example-chat-* --strip-components 1 -C ~/jazz-examples/chat
cd ~/jazz-examples/chat
```
This ensures that you have the example app without git history and independent of the Jazz multi-package monorepo.
Install dependencies:
```bash
pnpm install
```
Start the dev server:
```bash
pnpm dev
```
## Questions / problems / feedback
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
## Configuration: sync server
By default, the example app uses [Jazz Global Mesh](https://jazz.tools/mesh) (`wss://sync.jazz.tools`) - so cross-device use, invites and collaboration should just work.
You can also run a local sync server by running `npx cojson-simple-sync` and adding the query param `?sync=ws://localhost:4200` to the URL of the example app (for example: `http://localhost:5173/?sync=ws://localhost:4200`), or by setting the `sync` parameter of the `<Jazz.Provider>` provider component in [./src/2_main.tsx](./src/2_main.tsx).

View File

@@ -0,0 +1,14 @@
<!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>

View File

@@ -0,0 +1,56 @@
job "chat$BRANCH_SUFFIX" {
region = "global"
datacenters = ["*"]
group "static" {
count = 4
network {
port "http" {
to = 80
}
}
constraint {
attribute = "${node.class}"
operator = "="
value = "mesh"
}
spread {
attribute = "${node.datacenter}"
weight = 100
}
constraint {
distinct_hosts = true
}
task "server" {
driver = "docker"
config {
image = "$DOCKER_TAG"
ports = ["http"]
auth = {
username = "$DOCKER_USER"
password = "$DOCKER_PASSWORD"
}
}
service {
tags = ["public"]
name = "chat$BRANCH_SUFFIX"
port = "http"
provider = "consul"
}
resources {
cpu = 50 # MHz
memory = 50 # MB
}
}
}
}
# deploy bump 4

View File

@@ -0,0 +1,61 @@
{
"name": "jazz-example-richtext",
"private": true,
"version": "0.0.57",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"format": "echo 'chat example is codegolfed'",
"preview": "vite preview"
},
"lint-staged": {
"*.{ts,tsx}": "eslint --fix",
"*.{js,jsx,mdx,json}": "prettier --write"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.4",
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cojson": "workspace:*",
"hash-slash": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0",
"prosemirror-example-setup": "^1.2.2",
"prosemirror-menu": "^1.2.4",
"prosemirror-model": "^1.21.1",
"prosemirror-schema-basic": "^1.2.2",
"prosemirror-state": "^1.4.3",
"prosemirror-transform": "^1.9.0",
"prosemirror-view": "^1.33.7",
"qrcode": "^1.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"react-use": "^17.4.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"uniqolor": "^1.1.0"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.14",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.27",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^5.0.10"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -0,0 +1,313 @@
import { Group, ID, CoRichText, Marks, TreeNode, TreeLeaf } from "jazz-tools";
import { createJazzReactContext, DemoAuth } from "jazz-react";
import { createRoot } from "react-dom/client";
import { useIframeHashRouter } from "hash-slash";
import { useEffect, useState } from "react";
export class Document extends CoRichText {}
const Jazz = createJazzReactContext({
auth: DemoAuth({ appName: "Jazz Richtext Doc" }),
peer: `wss://mesh.jazz.tools/?key=you@example.com`,
});
export const { useAccount, useCoState } = Jazz;
function App() {
const { me, logOut } = useAccount();
const createDocument = () => {
const group = Group.create({ owner: me });
group.addMember("everyone", "writer");
const Doc = Document.createFromPlainTextAndMark("", Marks.Paragraph, {tag: "paragraph"}, { owner: me });
setTimeout(() => {
location.hash = "/doc/" + Doc.id;
}, 1000);
};
return (
<div className="flex flex-col items-center w-screen h-screen p-2 dark:bg-black dark:text-white">
<div className="rounded mb-5 px-2 py-1 text-sm self-end">
{me.profile?.name} · <button onClick={logOut}>Log Out</button>
</div>
{useIframeHashRouter().route({
"/": () => createDocument() as never,
"/doc/:id": (id) => (
<DocumentComponent docID={id as ID<Document>} />
),
})}
</div>
);
}
createRoot(document.getElementById("root")!).render(
<Jazz.Provider>
<App />
</Jazz.Provider>
);
import {
EditorState,
Transaction as ProsemirrorTransaction,
TextSelection,
} from "prosemirror-state";
import {
Node as ProsemirrorNode,
Mark as ProsemirrorMark,
} from "prosemirror-model";
import { ReplaceStep, AddMarkStep } from "prosemirror-transform";
import { EditorView } from "prosemirror-view";
import { schema } from "prosemirror-schema-basic";
import { exampleSetup } from "prosemirror-example-setup";
import "prosemirror-example-setup/style/style.css";
import "prosemirror-menu/style/menu.css";
import "prosemirror-view/style/prosemirror.css";
function DocumentComponent({ docID }: { docID: ID<Document> }) {
const { me } = useAccount();
const [mount, setMount] = useState<HTMLElement | null>(null);
console.log("rerendering");
useEffect(() => {
if (!mount) return;
console.log("Creating EditorView");
const setupPlugins = exampleSetup({ schema, history: false });
console.log("setupPlugins", setupPlugins, schema);
const editorView = new EditorView(mount, {
state: EditorState.create({
doc: schema.node("doc", undefined, [
schema.node("paragraph", undefined, undefined),
]),
schema: schema,
plugins: setupPlugins,
}),
dispatchTransaction(tr) {
const expectedNewState = editorView.state.apply(tr);
if (lastDoc) {
applyTxToPlainText(lastDoc, tr);
}
console.log(
"Setting view state to normal new state",
expectedNewState
);
editorView.updateState(expectedNewState);
},
});
let lastDoc: Document | undefined;
const unsub = Document.subscribe(
docID,
me,
{ text: true, marks: [[]] },
(doc) => {
lastDoc = doc;
console.log("Applying doc update");
console.log(
"marks",
doc.toString(),
doc.resolveAndDiffuseAndFocusMarks()
);
console.log("tree", doc.toTree(["strong", "em"]));
console.log(richTextToProsemirrorDoc(doc));
const focusedBefore = editorView.hasFocus();
editorView.updateState(
EditorState.create({
doc: richTextToProsemirrorDoc(doc),
plugins: editorView.state.plugins,
selection: editorView.state.selection,
schema: editorView.state.schema,
storedMarks: editorView.state.storedMarks,
})
);
if (focusedBefore) {
editorView.focus();
}
}
);
return () => {
console.log("Destroying");
editorView.destroy();
unsub();
};
}, [mount, docID, !!me]);
return (
<div>
<h1>Document</h1>
<div ref={setMount} className="border min-w-96 p-5 min-h-96" />
</div>
);
}
function richTextToProsemirrorDoc(
text: CoRichText
): ProsemirrorNode | undefined {
const asString = text.toString();
return schema.node("doc", undefined, [
schema.node(
"paragraph",
{ start: 0, end: asString.length },
asString.length === 0
? undefined
: text.toTree(["strong", "em"]).children.map((child) => {
if (
child.type === "leaf" ||
child.tag === "strong" ||
child.tag === "em"
) {
return collectInlineMarks(asString, child, []);
} else {
throw new Error("Unsupported tag " + child.tag);
}
})
),
]);
}
function collectInlineMarks(
fullString: string,
node: TreeNode | TreeLeaf,
currentMarks: ProsemirrorMark[]
) {
if (node.type === "leaf") {
return schema.text(
fullString.slice(node.start, node.end),
currentMarks
);
} else {
if (node.tag === "strong") {
return collectInlineMarks(
fullString,
node.children[0],
currentMarks.concat(schema.mark("strong"))
);
} else if (node.tag === "em") {
return collectInlineMarks(
fullString,
node.children[0],
currentMarks.concat(schema.mark("em"))
);
} else {
throw new Error("Unsupported tag " + node.tag);
}
}
}
function applyTxToPlainText(text: CoRichText, tr: ProsemirrorTransaction) {
console.log("transaction", tr);
for (const step of tr.steps) {
if (step instanceof ReplaceStep) {
const resolvedStart = tr.before.resolve(step.from);
const resolvedEnd = tr.before.resolve(step.to);
const selectionToStart = TextSelection.between(
tr.before.resolve(0),
resolvedStart
);
const start = selectionToStart
.content()
.content.textBetween(
0,
selectionToStart.content().content.size
).length;
const selectionToEnd = TextSelection.between(
tr.before.resolve(0),
resolvedEnd
);
const end = selectionToEnd
.content()
.content.textBetween(
0,
selectionToEnd.content().content.size
).length;
console.log(
"step",
step,
resolvedStart,
resolvedEnd,
selectionToStart,
start,
end
);
if (start === end) {
if (step.slice.content.firstChild?.text) {
text.insertAfter(start, step.slice.content.firstChild.text);
} else {
// this is a split operation
const splitNodeType =
step.slice.content.firstChild?.type.name;
if (splitNodeType === "paragraph") {
const matchingMarks =
text.marks?.filter(
(m): m is Exclude<typeof m, null> =>
!!m &&
m.tag === "paragraph" &&
(m.startAfter && text.idxAfter(m.startAfter) || 0) <
start &&
(m.endBefore && text.idxBefore(m.endBefore) || Infinity) >
start
) || [];
console.log("split before", start, matchingMarks);
let lastSeenEnd = start;
for (const matchingMark of matchingMarks) {
const originalEnd = text.idxAfter(
matchingMark.endAfter
)!; // TODO: non-tight case
if (originalEnd > lastSeenEnd) {
lastSeenEnd = originalEnd;
}
matchingMark.endBefore = text.posBefore(start + 1)!;
matchingMark.endAfter = text.posAfter(start)!;
}
console.log("split after", matchingMarks, lastSeenEnd);
text.insertMark(start, lastSeenEnd, Marks.Paragraph, {
tag: "paragraph",
});
} else {
console.warn(
"Unknown node type to split",
splitNodeType
);
}
}
} else {
text.deleteRange({ from: start, to: end });
}
} else if (step instanceof AddMarkStep) {
console.log("step", step);
if (step.mark.type.name === "strong") {
text.insertMark(step.from, step.to - 1, Marks.Strong, {
tag: "strong",
});
} else if (step.mark.type.name === "em") {
text.insertMark(step.from, step.to - 1, Marks.Em, {
tag: "em",
});
} else {
console.warn("Unsupported mark type", step.mark);
}
} else {
console.warn("Unsupported step type", step);
}
}
}

View File

@@ -0,0 +1,78 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--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%;
--primary: 24 9.8% 10%;
--primary-foreground: 60 9.1% 97.8%;
--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%;
--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%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 20 14.3% 4.1%;
--radius: 0.5rem;
}
.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%;
--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%;
--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%;
--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%;
--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;
}
}

1
examples/richtext/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,75 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}

View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import path from "path";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
build: {
minify: false
}
})

View File

@@ -1,5 +1,72 @@
# jazz-example-todo
## 0.0.87
### Patch Changes
- Updated dependencies
- jazz-react@0.7.23
- jazz-tools@0.7.23
## 0.0.86
### Patch Changes
- jazz-react@0.7.22
## 0.0.85
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.21
- jazz-react@0.7.21
## 0.0.84
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.20
- jazz-react@0.7.20
## 0.0.83
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.19
- jazz-react@0.7.19
## 0.0.82
### Patch Changes
- jazz-react@0.7.18
- jazz-tools@0.7.18
## 0.0.81
### Patch Changes
- jazz-react@0.7.17
- jazz-tools@0.7.17
## 0.0.80
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.16
- jazz-react@0.7.16
## 0.0.79
### Patch Changes
- Updated dependencies
- jazz-react@0.7.15
## 0.0.78
### Patch Changes

View File

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

View File

@@ -9,9 +9,11 @@ import localFont from "next/font/local";
import { GcmpLogo, JazzLogo } from "@/components/logos";
import { SiGithub, SiDiscord, SiTwitter } from "@icons-pack/react-simple-icons";
import { Nav, NavLink, Newsletter, NewsletterButton } from "@/components/nav";
import { MailIcon } from "lucide-react";
import { DocNav } from "@/components/docs/nav";
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({
subsets: ["latin"],
@@ -48,6 +50,8 @@ export default function RootLayout({
"flex flex-col items-center bg-stone-50 dark:bg-stone-950 overflow-x-hidden",
].join(" ")}
>
<SpeedInsights />
<Analytics />
<ThemeProvider
attribute="class"
defaultTheme="system"
@@ -108,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>
@@ -192,12 +196,6 @@ export default function RootLayout({
</div>
</footer>
</ThemeProvider>
<script
defer
data-api="/api/event"
data-domain="jazz.tools"
src="/js/script.js"
></script>
</body>
</html>
);

View File

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

View File

@@ -14,6 +14,7 @@
"*.{ts,tsx}": "eslint --fix",
"*.{js,jsx,mdx,json}": "prettier --write"
},
"packageManager": "pnpm@9.1.4",
"dependencies": {
"@evilmartians/harmony": "^1.0.0",
"@icons-pack/react-simple-icons": "^9.1.0",
@@ -21,6 +22,8 @@
"@mdx-js/react": "^2.3.0",
"@next/mdx": "^13.5.4",
"@types/mdx": "^2.0.8",
"@vercel/analytics": "^1.3.1",
"@vercel/speed-insights": "^1.0.12",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-react": "^0.284.0",

5244
homepage/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
# jazz-example-chat
## 0.0.47
### Patch Changes
- Updated dependencies
- cojson@0.6.7
- jazz-react@0.5.5
- jazz-react-auth-local@0.4.18
## 0.0.46
### Patch Changes
- Updated dependencies
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16

View File

@@ -1,243 +0,0 @@
import {
WithJazz,
useJazz,
DemoAuth,
useAutoSub,
useBinaryStream,
} from "jazz-react";
import ReactDOM from "react-dom/client";
import { HashRoute } from "hash-slash";
import { Account, CoID, CoValue, SessionID } from "cojson";
import { clsx } from "clsx";
import { ImageDefinition } from "cojson/src/media";
import { CoJsonTree } from "./cojson-tree";
ReactDOM.createRoot(document.getElementById("root")!).render(
<WithJazz
auth={DemoAuth({ appName: "Jazz Chat Example" })}
apiKey="api_z9d034j3t34ht034ir"
>
<App />
</WithJazz>
);
function App() {
return (
<div className="flex flex-col items-center justify-between w-screen h-screen p-2 ">
<button
onClick={useJazz().logOut}
className="rounded mb-5 px-2 py-1 bg-stone-200 dark:bg-stone-800 dark:text-white self-end"
>
Log Out
</button>
{HashRoute(
{
"/": <Home />,
"/:id": (id) => <Inspect coValueId={id as CoID<CoValue>} />,
},
{ reportToParentFrame: true }
)}
</div>
);
}
function Home() {
return (
<form
className="mb-auto"
onSubmit={(event) => {
const coValueId = (event.target as any).coValueId
.value as CoID<CoValue>;
location.hash = "/" + coValueId;
event.preventDefault();
}}
>
<input name="coValueId" className="border" />
<button>Inspect</button>
</form>
);
}
function Tag({ children, href }: { children: React.ReactNode; href?: string }) {
if (href) {
return (
<a
href={href}
className="border text-xs px-2 py-0.5 rounded hover:underline"
>
{children}
</a>
);
}
return <span className="border text-xs px-2 py-0.5 rounded">{children}</span>;
}
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 image = useBinaryStream(idToResolve);
return (
<img src={image?.blobURL || value.placeholderDataURL} alt="placeholder" />
);
}
function Inspect({ coValueId }: { coValueId: CoID<CoValue> }) {
const coValue = useAutoSub(coValueId);
const values = coValue?.meta.coValue.toJSON() || {};
const isImage = "placeholderDataURL" in values;
const isGroup = coValue?.meta.group.id === coValueId;
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<Account>} />
</div>
))}
</div>
</p>
) : (
<span className="">
Group{" "}
<Tag href={`#/${coValue?.meta.group.id}`}>
{coValue?.meta.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} />
</pre>
<h2 className="text-lg font-semibold mt-10 mb-4">Sessions</h2>
{coValue && <Sessions coValue={coValue.meta.coValue} />}
</div>
);
}
function Sessions({ coValue }: { coValue: CoValue }) {
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}
/>
</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,
}: {
sessionID: SessionID;
transactionCount: number;
}) {
let Prefix = sessionID.startsWith("co_") ? (
<AccountInfo accountID={sessionID.split("_session_")[0] as CoID<Account>} />
) : (
<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>
);
}
function AccountInfo({ accountID }: { accountID: CoID<Account> }) {
const account = useAutoSub(accountID);
return (
<div className="flex items-center gap-2">
<h1>{account?.profile?.name}</h1>
<Tag href={`#/${accountID}`}>{account?.id}</Tag>
</div>
);
}

View File

@@ -1,151 +0,0 @@
import clsx from "clsx";
import { CoID, CoValue } from "cojson";
import { useAutoSub } from "jazz-react";
import { useState } from "react";
import { LinkIcon } from "./link-icon";
export function CoJsonTree({ coValueId }: { coValueId: CoID<CoValue> }) {
const coValue = useAutoSub(coValueId);
const values = coValue?.meta.coValue.toJSON() || {};
return <RenderCoValueJSON json={values} />;
}
function RenderObject({ json }: { json: Record<string, any> }) {
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} />;
})}
{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,
}: {
property: string;
value: any;
}) {
const [shouldLoad, setShouldLoad] = useState(false);
const isCoValue =
typeof value === "string" ? value?.startsWith("co_") : false;
return (
<div className={clsx(`flex group`)}>
<span className="text-gray-500 flex">
<RenderCoValueJSON json={property} />:{" "}
</span>
{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} /> : null}</span>
</div>
) : (
<div className="">
<RenderCoValueJSON json={value} />
</div>
)}
</div>
);
}
function RenderCoValueArray({ json }: { json: any[] }) {
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} />
</div>
);
})}
{hasMore ? (
<div
className="text-gray-500 cursor-pointer"
onClick={() => setLimit((l) => l + 10)}
>
... {json.length - limit} more
</div>
) : null}
</div>
);
}
function RenderCoValueJSON({
json,
}: {
json:
| Record<string, any>
| any[]
| string
| null
| number
| boolean
| undefined;
}) {
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} />
</div>
<span className="text-gray-500">]</span>
</div>
);
} else if (
typeof json === "object" &&
json &&
Object.getPrototypeOf(json) === Object.prototype
) {
return <RenderObject json={json} />;
} else if (typeof json === "string") {
if (json?.startsWith("co_")) {
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>;
}
}

View File

@@ -6,10 +6,9 @@
"packages/*",
"examples/*"
],
"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",
@@ -24,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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,23 @@
# cojson
## 0.7.23
### Patch Changes
- Mostly complete OPFS implementation (single-tab only)
## 0.7.18
### Patch Changes
- Update to Effect 3.5.2
## 0.7.17
### Patch Changes
- Fix bugs in new storage interface
## 0.7.14
### Patch Changes

View File

@@ -5,7 +5,7 @@
"types": "src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.7.14",
"version": "0.7.23",
"devDependencies": {
"@types/jest": "^29.5.3",
"@typescript-eslint/eslint-plugin": "^6.2.1",
@@ -22,7 +22,7 @@
"@noble/ciphers": "^0.1.3",
"@noble/hashes": "^1.4.0",
"@scure/base": "^1.1.1",
"effect": "^3.1.5",
"effect": "^3.5.2",
"hash-wasm": "^4.9.0"
},
"scripts": {

View File

@@ -6,6 +6,7 @@ import { RawCoList } from "./coValues/coList.js";
import { CoValueCore } from "./coValueCore.js";
import { RawGroup } from "./coValues/group.js";
import { RawAccount, Profile } from "./index.js";
import { RawCoPlainText } from "./coValues/coPlainText.js";
export type CoID<T extends RawCoValue> = RawCoID & {
readonly __type: T;
@@ -66,3 +67,11 @@ export function expectStream(content: RawCoValue): RawCoStream {
return content as RawCoStream;
}
export function expectPlainText(content: RawCoValue): RawCoPlainText {
if (content.type !== "coplaintext") {
throw new Error("Expected plaintext");
}
return content as RawCoPlainText;
}

View File

@@ -7,9 +7,9 @@ import { AgentID, SessionID, TransactionID } from "../ids.js";
import { AccountID } from "./account.js";
import { RawGroup } from "./group.js";
type OpID = TransactionID & { changeIdx: number };
export type OpID = TransactionID & { changeIdx: number };
type InsertionOpPayload<T extends JsonValue> =
export type InsertionOpPayload<T extends JsonValue> =
| {
op: "pre";
value: T;
@@ -21,7 +21,7 @@ type InsertionOpPayload<T extends JsonValue> =
after: OpID | "start";
};
type DeletionOpPayload = {
export type DeletionOpPayload = {
op: "del";
insertion: OpID;
};
@@ -49,7 +49,7 @@ export class RawCoListView<
/** @category 6. Meta */
id: CoID<this>;
/** @category 6. Meta */
type = "colist" as const;
type: "colist" | "coplaintext" = "colist" as const;
/** @category 6. Meta */
core: CoValueCore;
/** @internal */
@@ -446,13 +446,7 @@ export class RawCoList<
privacy,
);
const listAfter = new RawCoList(this.core) as this;
this.afterStart = listAfter.afterStart;
this.beforeEnd = listAfter.beforeEnd;
this.insertions = listAfter.insertions;
this.deletionsByInsertion = listAfter.deletionsByInsertion;
this._cachedEntries = undefined;
this.rebuildFromCore();
}
/**
@@ -499,13 +493,7 @@ export class RawCoList<
privacy,
);
const listAfter = new RawCoList(this.core) as this;
this.afterStart = listAfter.afterStart;
this.beforeEnd = listAfter.beforeEnd;
this.insertions = listAfter.insertions;
this.deletionsByInsertion = listAfter.deletionsByInsertion;
this._cachedEntries = undefined;
this.rebuildFromCore();
}
/** Deletes the item at index `at`.
@@ -532,13 +520,7 @@ export class RawCoList<
privacy,
);
const listAfter = new RawCoList(this.core) as this;
this.afterStart = listAfter.afterStart;
this.beforeEnd = listAfter.beforeEnd;
this.insertions = listAfter.insertions;
this.deletionsByInsertion = listAfter.deletionsByInsertion;
this._cachedEntries = undefined;
this.rebuildFromCore();
}
replace(
@@ -566,6 +548,11 @@ export class RawCoList<
],
privacy,
);
this.rebuildFromCore();
}
/** @internal */
rebuildFromCore() {
const listAfter = new RawCoList(this.core) as this;
this.afterStart = listAfter.afterStart;

View File

@@ -0,0 +1,129 @@
import { CoValueCore } from "../coValueCore.js";
import { JsonObject } from "../jsonValue.js";
import { DeletionOpPayload, InsertionOpPayload, OpID, RawCoList } from "./coList.js";
export type StringifiedOpID = string & { __stringifiedOpID: true };
export function stringifyOpID(opID: OpID): StringifiedOpID {
return `${opID.sessionID}:${opID.txIndex}:${opID.changeIdx}` as StringifiedOpID;
}
type PlaintextIdxMapping = {
opIDbeforeIdx: OpID[];
opIDafterIdx: OpID[];
idxAfterOpID: { [opID: StringifiedOpID]: number };
idxBeforeOpID: { [opID: StringifiedOpID]: number };
};
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
export class RawCoPlainText<
Meta extends JsonObject | null = JsonObject | null,
> extends RawCoList<string, Meta> {
/** @category 6. Meta */
type = "coplaintext" as const;
_cachedMapping: WeakMap<
NonNullable<typeof this._cachedEntries>,
PlaintextIdxMapping
>;
constructor(core: CoValueCore) {
super(core);
this._cachedMapping = new WeakMap();
}
get mapping() {
const entries = this.entries();
let mapping = this._cachedMapping.get(entries);
if (mapping) {
return mapping;
}
mapping = {
opIDbeforeIdx: [],
opIDafterIdx: [],
idxAfterOpID: {},
idxBeforeOpID: {},
};
let idxBefore = 0;
for (const entry of entries) {
const idxAfter = idxBefore + entry.value.length;
mapping.opIDafterIdx[idxBefore] = entry.opID;
mapping.opIDbeforeIdx[idxAfter] = entry.opID;
mapping.idxAfterOpID[stringifyOpID(entry.opID)] = idxAfter;
mapping.idxBeforeOpID[stringifyOpID(entry.opID)] = idxBefore;
idxBefore = idxAfter;
}
this._cachedMapping.set(entries, mapping);
return mapping;
}
toString() {
return this.entries()
.map((entry) => entry.value)
.join("");
}
insertAfter(
idx: number,
text: string,
privacy: "private" | "trusting" = "private"
) {
const ops: InsertionOpPayload<string>[] = [];
let prevOpId: OpID | "start" | undefined = this.mapping.opIDbeforeIdx[idx];
if (!prevOpId) {
if (idx === 0) {
prevOpId = "start"
} else {
throw new Error("Invalid idx");
}
}
const nextTxId = this.core.nextTransactionID();
let changeIdx = 0;
for (const grapheme of segmenter.segment(text)) {
ops.push({
op: "app",
value: grapheme.segment,
after: prevOpId,
});
prevOpId = {
sessionID: nextTxId.sessionID,
txIndex: nextTxId.txIndex,
changeIdx,
};
changeIdx++;
}
this.core.makeTransaction(ops, privacy);
this.rebuildFromCore();
}
deleteRange({from, to}: {from: number, to: number}, privacy: "private" | "trusting" = "private") {
const ops: DeletionOpPayload[] = [];
for (let idx = from; idx < to;) {
const insertion = this.mapping.opIDafterIdx[idx];
if (!insertion) {
throw new Error("Invalid idx to delete " + (idx));
}
ops.push({
op: "del",
insertion,
});
console.log("deleting idx", idx)
let nextIdx = idx + 1;
while (!this.mapping.opIDbeforeIdx[nextIdx] && nextIdx < to) {
nextIdx++;
}
idx = nextIdx;
}
this.core.makeTransaction(ops, privacy);
this.rebuildFromCore();
}
}

View File

@@ -8,6 +8,7 @@ import { AgentID, isAgentID } from "../ids.js";
import { RawAccount, AccountID, ControlledAccountOrAgent } from "./account.js";
import { Role } from "../permissions.js";
import { base58 } from "@scure/base";
import { RawCoPlainText } from "./coPlainText.js";
export const EVERYONE = "everyone" as const;
export type Everyone = "everyone";
@@ -307,6 +308,36 @@ export class RawGroup<
return list;
}
/**
* Creates a new `CoList` within this group, with the specified specialized
* `CoList` type `L` and optional static metadata.
*
* @category 3. Value creation
*/
createPlainText<T extends RawCoPlainText>(
init?: string,
meta?: T["headerMeta"],
initPrivacy: "trusting" | "private" = "private"
): T {
const text = this.core.node
.createCoValue({
type: "coplaintext",
ruleset: {
type: "ownedByGroup",
group: this.id,
},
meta: meta || null,
...this.core.crypto.createdNowUnique(),
})
.getCurrentContent() as T;
if (init) {
text.insertAfter(0, init, initPrivacy);
}
return text;
}
/** @category 3. Value creation */
createStream<C extends RawCoStream>(meta?: C["headerMeta"]): C {
return this.core.node

View File

@@ -5,6 +5,7 @@ import { RawCoMap } from "./coValues/coMap.js";
import { RawCoList } from "./coValues/coList.js";
import { RawCoStream } from "./coValues/coStream.js";
import { RawBinaryCoStream } from "./coValues/coStream.js";
import { RawCoPlainText } from "./coValues/coPlainText.js";
export function coreToCoValue(
core: CoValueCore,
@@ -38,6 +39,8 @@ export function coreToCoValue(
} else {
return new RawCoStream(core);
}
} else if (core.header.type === "coplaintext") {
return new RawCoPlainText(core);
} else {
throw new Error(`Unknown coValue type ${core.header.type}`);
}

View File

@@ -9,6 +9,7 @@ import { LocalNode } from "./localNode.js";
import type { RawCoValue } from "./coValue.js";
import { RawCoMap } from "./coValues/coMap.js";
import { RawCoList } from "./coValues/coList.js";
import { RawCoPlainText, stringifyOpID } from "./coValues/coPlainText.js";
import { RawCoStream, RawBinaryCoStream } from "./coValues/coStream.js";
import {
secretSeedLength,
@@ -94,6 +95,7 @@ export {
Everyone,
RawCoMap,
RawCoList,
RawCoPlainText,
RawCoStream,
RawBinaryCoStream,
RawCoValue,
@@ -122,6 +124,7 @@ export {
PureJSCrypto,
SyncMessage,
isRawCoID,
stringifyOpID,
LSMStorage,
DisconnectedError,
PingTimeoutError,
@@ -153,4 +156,5 @@ export namespace CojsonInternalTypes {
export type SealerSecret = import("./crypto/crypto.js").SealerSecret;
export type SignerSecret = import("./crypto/crypto.js").SignerSecret;
export type JsonObject = import("./jsonValue.js").JsonObject;
export type OpID = import("./coValues/coList.js").OpID;
}

View File

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

View File

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

View File

@@ -47,18 +47,24 @@ export function newQueuePair(
const queue = yield* Queue.unbounded<SyncMessage>();
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,
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];
queue,
];
} else {
return [Stream.fromQueue(queue), queue];
}

View File

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

View File

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

View File

@@ -0,0 +1,73 @@
import { expect, test } from "vitest";
import { expectPlainText } from "../coValue.js";
import { WasmCrypto } from "../index.js";
import { LocalNode } from "../localNode.js";
import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
const Crypto = await WasmCrypto.create();
test("Empty CoPlainText works", () => {
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
const coValue = node.createCoValue({
type: "coplaintext",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...Crypto.createdNowUnique(),
});
const content = expectPlainText(coValue.getCurrentContent());
expect(content.type).toEqual("coplaintext");
expect(content.toString()).toEqual("");
});
test("Can insert into empty CoPlainText", () => {
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
const coValue = node.createCoValue({
type: "coplaintext",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...Crypto.createdNowUnique(),
});
const content = expectPlainText(coValue.getCurrentContent());
expect(content.type).toEqual("coplaintext");
content.insertAfter(0, "a", "trusting");
expect(content.toString()).toEqual("a");
});
test("Can insert and delete in CoPlainText", () => {
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
const coValue = node.createCoValue({
type: "coplaintext",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...Crypto.createdNowUnique(),
});
const content = expectPlainText(coValue.getCurrentContent());
expect(content.type).toEqual("coplaintext");
content.insertAfter(0, "hello", "trusting");
expect(content.toString()).toEqual("hello");
content.insertAfter(5, " world", "trusting");
expect(content.toString()).toEqual("hello world");
console.log("first delete")
content.deleteRange({from: 3, to: 8}, "trusting");
expect(content.toString()).toEqual("helrld");
content.insertAfter(2, "😍", "trusting");
expect(content.toString()).toEqual("he😍lrld")
console.log("second delete")
content.deleteRange({from: 2, to: 4}, "trusting");
expect(content.toString()).toEqual("helrld");
})

View File

@@ -1,5 +1,65 @@
# jazz-browser-media-images
## 0.7.23
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.23
- jazz-browser@0.7.23
## 0.7.22
### Patch Changes
- jazz-browser@0.7.22
## 0.7.21
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.21
- jazz-browser@0.7.21
## 0.7.20
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.20
- jazz-browser@0.7.20
## 0.7.19
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.19
- jazz-browser@0.7.19
## 0.7.18
### Patch Changes
- jazz-browser@0.7.18
- jazz-tools@0.7.18
## 0.7.17
### Patch Changes
- jazz-browser@0.7.17
- jazz-tools@0.7.17
## 0.7.16
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.16
- jazz-browser@0.7.16
## 0.7.14
### Patch Changes

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,10 +10,7 @@ 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";
@@ -39,7 +36,7 @@ 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());

View File

@@ -1,5 +1,67 @@
# jazz-autosub
## 0.7.23
### Patch Changes
- Updated dependencies
- cojson@0.7.23
- jazz-tools@0.7.23
- cojson-transport-ws@0.7.23
## 0.7.22
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.22
## 0.7.21
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.21
## 0.7.20
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.20
## 0.7.19
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.19
## 0.7.18
### Patch Changes
- Updated dependencies
- cojson@0.7.18
- cojson-transport-ws@0.7.18
- jazz-tools@0.7.18
## 0.7.17
### Patch Changes
- Updated dependencies
- cojson@0.7.17
- cojson-transport-ws@0.7.17
- jazz-tools@0.7.17
## 0.7.16
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.16
## 0.7.14
### Patch Changes

View File

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

View File

@@ -1,5 +1,77 @@
# jazz-react
## 0.7.23
### Patch Changes
- Mostly complete OPFS implementation (single-tab only)
- Updated dependencies
- cojson@0.7.23
- jazz-tools@0.7.23
- jazz-browser@0.7.23
## 0.7.22
### Patch Changes
- jazz-browser@0.7.22
## 0.7.21
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.21
- jazz-browser@0.7.21
## 0.7.20
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.20
- jazz-browser@0.7.20
## 0.7.19
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.19
- jazz-browser@0.7.19
## 0.7.18
### Patch Changes
- Updated dependencies
- cojson@0.7.18
- jazz-browser@0.7.18
- jazz-tools@0.7.18
## 0.7.17
### Patch Changes
- Updated dependencies
- cojson@0.7.17
- jazz-browser@0.7.17
- jazz-tools@0.7.17
## 0.7.16
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.16
- jazz-browser@0.7.16
## 0.7.15
### Patch Changes
- Provide current res in ProgressiveImg
## 0.7.14
### Patch Changes

View File

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

View File

@@ -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

View File

@@ -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<
| {
@@ -127,20 +127,20 @@ export function createJazzReactContext<Acc extends Account>({
id: ID<V> | undefined,
depth: D & DepthsIn<V> = [] as D & DepthsIn<V>,
): DeeplyLoaded<V, D> | undefined {
const [state, setState] = useState<DeeplyLoaded<V, D> | undefined>(
undefined,
);
const [state, setState] = useState<{
value: DeeplyLoaded<V, D> | undefined;
}>({ value: undefined });
const me = React.useContext(JazzContext)?.me;
useEffect(() => {
if (!id || !me) return;
return subscribeToCoValue(Schema, id, me, depth, (value) => {
setState(value);
setState({ value });
});
}, [Schema, id, me]);
return state;
return state.value;
}
function useAcceptInvite<V extends CoValue>({

View File

@@ -9,7 +9,10 @@ export function useProgressiveImg({
image: ImageDefinition | null | undefined;
maxWidth?: number;
}) {
const [src, setSrc] = useState<string | undefined>(undefined);
const [current, setCurrent] = useState<
| { src?: string; res?: `${number}x${number}` | "placeholder" }
| undefined
>(undefined);
useEffect(() => {
let lastHighestRes: string | undefined;
@@ -22,21 +25,28 @@ export function useProgressiveImg({
const blob = highestRes.stream.toBlob();
if (blob) {
const blobURI = URL.createObjectURL(blob);
setSrc(blobURI);
setCurrent({ src: blobURI, res: highestRes.res });
return () => {
setTimeout(() => URL.revokeObjectURL(blobURI), 200);
};
}
}
} else {
setSrc(update?.placeholderDataURL);
setCurrent({
src: update?.placeholderDataURL,
res: "placeholder",
});
}
});
return unsub;
}, [image?.id, maxWidth]);
return { src, originalSize: image?.originalSize };
return {
src: current?.src,
res: current?.res,
originalSize: image?.originalSize,
};
}
/** @category Media */
@@ -47,6 +57,7 @@ export function ProgressiveImg({
}: {
children: (result: {
src: string | undefined;
res: `${number}x${number}` | "placeholder" | undefined;
originalSize: readonly [number, number] | undefined;
}) => React.ReactNode;
image: ImageDefinition | null | undefined;

View File

@@ -1,5 +1,67 @@
# jazz-autosub
## 0.7.23
### Patch Changes
- Updated dependencies
- cojson@0.7.23
- jazz-tools@0.7.23
- cojson-transport-ws@0.7.23
## 0.7.22
### Patch Changes
- Updated dependencies
- cojson-transport-ws@0.7.22
## 0.7.21
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.21
## 0.7.20
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.20
## 0.7.19
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.19
## 0.7.18
### Patch Changes
- Updated dependencies
- cojson@0.7.18
- cojson-transport-ws@0.7.18
- jazz-tools@0.7.18
## 0.7.17
### Patch Changes
- Updated dependencies
- cojson@0.7.17
- cojson-transport-ws@0.7.17
- jazz-tools@0.7.17
## 0.7.16
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.16
## 0.7.14
### Patch Changes

View File

@@ -3,7 +3,7 @@
"bin": "./dist/index.js",
"type": "module",
"license": "MIT",
"version": "0.7.14",
"version": "0.7.23",
"scripts": {
"lint": "eslint . --ext ts,tsx",
"format": "prettier --write './src/**/*.{ts,tsx}'",
@@ -16,7 +16,7 @@
"@effect/schema": "^0.66.16",
"cojson": "workspace:*",
"cojson-transport-ws": "workspace:*",
"effect": "^3.1.5",
"effect": "^3.5.2",
"fast-check": "^3.17.2",
"jazz-tools": "workspace:*",
"ws": "^8.14.2"

View File

@@ -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,

View File

@@ -1,5 +1,51 @@
# jazz-autosub
## 0.7.23
### Patch Changes
- Mostly complete OPFS implementation (single-tab only)
- Updated dependencies
- cojson@0.7.23
## 0.7.21
### Patch Changes
- Fix another bug in CoMap 'has' proxy trap
## 0.7.20
### Patch Changes
- Fix bug in CoMap 'has' trap
## 0.7.19
### Patch Changes
- Add support for "in" operator in CoMaps
## 0.7.18
### Patch Changes
- Updated dependencies
- cojson@0.7.18
## 0.7.17
### Patch Changes
- Updated dependencies
- cojson@0.7.17
## 0.7.16
### Patch Changes
- Fix: allow null in encoded fields
## 0.7.14
### Patch Changes

View File

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

View File

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

View File

@@ -539,6 +539,7 @@ const CoMapProxyHandler: ProxyHandler<CoMap> = {
if (
(typeof key === "string" || ItemsSym) &&
typeof value === "object" &&
value !== null &&
SchemaInit in value
) {
(target.constructor as typeof CoMap)._schema ||= {};
@@ -609,6 +610,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;

View File

@@ -0,0 +1,211 @@
import type { CojsonInternalTypes, RawCoPlainText } from "cojson";
import { RawAccount, stringifyOpID } from "cojson";
import type {
AccountCtx,
CoValue,
CoValueClass,
ID,
UnavailableError,
} from "../internal.js";
import { Account, Group, inspect, loadCoValue, loadCoValueEf, subscribeToCoValue, subscribeToCoValueEf, subscribeToExistingCoValue } from "../internal.js";
import type { Effect, Stream } from "effect";
export type TextPos = CojsonInternalTypes.OpID;
export class CoPlainText
extends String
implements CoValue
{
declare id: ID<this>;
declare _type: "CoPlainText";
declare _raw: RawCoPlainText;
get _owner(): Account | Group {
return this._raw.group instanceof RawAccount
? Account.fromRaw(this._raw.group)
: Group.fromRaw(this._raw.group);
}
get _loadedAs() {
return Account.fromNode(this._raw.core.node);
}
constructor(options: { fromRaw: RawCoPlainText } | { text: string, owner: Account | Group }) {
super();
let raw;
if ("fromRaw" in options) {
raw = options.fromRaw;
} else {
raw = options.owner._raw.createPlainText(options.text);
}
Object.defineProperties(this, {
id: { value: raw.id, enumerable: false },
_type: { value: "CoPlainText", enumerable: false },
_raw: { value: raw, enumerable: false },
});
}
static create<T extends CoPlainText>(this: CoValueClass<T>, text: string, options: { owner: Account | Group }) {
return new this({ text, owner: options.owner });
}
toString() {
return this._raw.toString();
}
valueOf() {
return this._raw.toString();
}
toJSON(): string {
return this._raw.toString();
}
[inspect]() {
return this.toJSON();
}
insertAfter(idx: number, text: string) {
this._raw.insertAfter(idx, text);
}
deleteRange(range: {from: number, to: number}) {
this._raw.deleteRange(range);
}
posBefore(idx: number): TextPos | undefined {
return this._raw.mapping.opIDbeforeIdx[idx];
}
posAfter(idx: number): TextPos | undefined {
return this._raw.mapping.opIDafterIdx[idx];
}
idxBefore(pos: TextPos): number | undefined {
return this._raw.mapping.idxBeforeOpID[stringifyOpID(pos)];
}
idxAfter(pos: TextPos): number | undefined {
return this._raw.mapping.idxAfterOpID[stringifyOpID(pos)];
}
static fromRaw<V extends CoPlainText>(
this: CoValueClass<V> & typeof CoPlainText,
raw: RawCoPlainText
) {
return new this({ fromRaw: raw });
}
/**
* Load a `CoPlainText` with a given ID, as a given account.
*
* `depth` specifies which (if any) fields that reference other CoValues to load as well before resolving.
* The `DeeplyLoaded` return type guarantees that corresponding referenced CoValues are loaded to the specified depth.
*
* You can pass `[]` or `{}` for shallowly loading only this CoPlainText, or `{ fieldA: depthA, fieldB: depthB }` for recursively loading referenced CoValues.
*
* Check out the `load` methods on `CoMap`/`CoList`/`CoStream`/`Group`/`Account` to see which depth structures are valid to nest.
*
* @example
* ```ts
* const person = await Person.load(
* "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
* me,
* { pet: {} }
* );
* ```
*
* @category Subscription & Loading
*/
static load<T extends CoPlainText>(
this: CoValueClass<T>,
id: ID<T>,
as: Account,
): Promise<T | undefined> {
return loadCoValue(this, id, as, []);
}
/**
* Effectful version of `CoMap.load()`.
*
* Needs to be run inside an `AccountCtx` context.
*
* @category Subscription & Loading
*/
static loadEf<T extends CoPlainText>(
this: CoValueClass<T>,
id: ID<T>,
): Effect.Effect<T, UnavailableError, AccountCtx> {
return loadCoValueEf(this, id, []);
}
/**
* Load and subscribe to a `CoMap` with a given ID, as a given account.
*
* Automatically also subscribes to updates to all referenced/nested CoValues as soon as they are accessed in the listener.
*
* `depth` specifies which (if any) fields that reference other CoValues to load as well before calling `listener` for the first time.
* The `DeeplyLoaded` return type guarantees that corresponding referenced CoValues are loaded to the specified depth.
*
* You can pass `[]` or `{}` for shallowly loading only this CoMap, or `{ fieldA: depthA, fieldB: depthB }` for recursively loading referenced CoValues.
*
* Check out the `load` methods on `CoMap`/`CoList`/`CoStream`/`Group`/`Account` to see which depth structures are valid to nest.
*
* Returns an unsubscribe function that you should call when you no longer need updates.
*
* Also see the `useCoState` hook to reactively subscribe to a CoValue in a React component.
*
* @example
* ```ts
* const unsub = Person.subscribe(
* "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
* me,
* { pet: {} },
* (person) => console.log(person)
* );
* ```
*
* @category Subscription & Loading
*/
static subscribe<T extends CoPlainText>(
this: CoValueClass<T>,
id: ID<T>,
as: Account,
listener: (value: T) => void,
): () => void {
return subscribeToCoValue(this, id, as, [], 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<T extends CoPlainText>(
this: CoValueClass<T>,
id: ID<T>,
): Stream.Stream<T, UnavailableError, AccountCtx> {
return subscribeToCoValueEf(this, id, []);
}
/**
* Given an already loaded `CoMap`, subscribe to updates to the `CoMap` and ensure that the specified fields are loaded to the specified depth.
*
* Works like `CoMap.subscribe()`, but you don't need to pass the ID or the account to load as again.
*
* Returns an unsubscribe function that you should call when you no longer need updates.
*
* @category Subscription & Loading
**/
subscribe<T extends CoPlainText>(
this: T,
listener: (value: T) => void,
): () => void {
return subscribeToExistingCoValue(this, [], listener);
}
}

View File

@@ -0,0 +1,396 @@
import type { Account, CoMapInit, Group, TextPos } from "../internal.js";
import { CoList, CoMap, CoPlainText, co } from "../internal.js";
export class Mark extends CoMap {
startAfter = co.json<TextPos | null>();
startBefore = co.json<TextPos>();
endAfter = co.json<TextPos>();
endBefore = co.json<TextPos | null>();
tag = co.string;
}
export type ResolvedMark<R extends Mark = Mark> = {
startAfter: number;
startBefore: number;
endAfter: number;
endBefore: number;
sourceMark: R;
};
export type ResolvedAndDiffusedMark<R extends Mark = Mark> = {
start: number;
end: number;
side: "uncertainStart" | "certainMiddle" | "uncertainEnd";
sourceMark: R;
};
export type FocusBias = "far" | "close" | "closestWhitespace";
export type ResolvedAndFocusedMark<R extends Mark = Mark> = {
start: number;
end: number;
sourceMark: R;
};
export class CoRichText extends CoMap {
text = co.ref(CoPlainText);
marks = co.ref(CoList.Of(co.ref(Mark)));
static createFromPlainText(
text: string,
options: { owner: Account | Group },
) {
return this.create(
{
text: CoPlainText.create(text, { owner: options.owner }),
marks: CoList.Of(co.ref(Mark)).create([], {
owner: options.owner,
}),
},
{ owner: options.owner },
);
}
static createFromPlainTextAndMark<MarkClass extends {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new (...args: any[]): Mark;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
create(init: any, options: { owner: Account | Group }): Mark;
}>(
text: string,
WrapIn: MarkClass,
extraArgs: Omit<
CoMapInit<InstanceType<MarkClass>>,
"startAfter" | "startBefore" | "endAfter" | "endBefore"
>,
options: { owner: Account | Group },
) {
const richtext = this.createFromPlainText(text, options);
richtext.insertMark(0, text.length, WrapIn, extraArgs);
return richtext;
}
insertAfter(idx: number, text: string) {
if (!this.text)
throw new Error(
"Cannot insert into a CoRichText without loaded text",
);
this.text.insertAfter(idx, text);
}
deleteRange(range: { from: number; to: number }) {
if (!this.text)
throw new Error(
"Cannot delete from a CoRichText without loaded text",
);
this.text.deleteRange(range);
}
posBefore(idx: number): TextPos | undefined {
if (!this.text)
throw new Error(
"Cannot get posBefore in a CoRichText without loaded text",
);
return this.text.posBefore(idx);
}
posAfter(idx: number): TextPos | undefined {
if (!this.text)
throw new Error(
"Cannot get posAfter in a CoRichText without loaded text",
);
return this.text.posAfter(idx);
}
idxBefore(pos: TextPos): number | undefined {
if (!this.text)
throw new Error(
"Cannot get idxBefore in a CoRichText without loaded text",
);
return this.text.idxBefore(pos);
}
idxAfter(pos: TextPos): number | undefined {
if (!this.text)
throw new Error(
"Cannot get idxAfter in a CoRichText without loaded text",
);
return this.text.idxAfter(pos);
}
insertMark<
MarkClass extends {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new (...args: any[]): Mark;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
create(init: any, options: { owner: Account | Group }): Mark;
},
>(
start: number,
end: number,
RangeClass: MarkClass,
extraArgs: Omit<
CoMapInit<InstanceType<MarkClass>>,
"startAfter" | "startBefore" | "endAfter" | "endBefore"
>,
options?: { markOwner?: Account | Group },
) {
if (!this.marks) {
throw new Error("Cannot insert a range without loaded ranges");
}
const range = RangeClass.create(
{
...extraArgs,
startAfter: this.posBefore(start),
startBefore: this.posAfter(start),
endAfter: this.posBefore(end),
endBefore: this.posAfter(end),
},
{ owner: options?.markOwner || this._owner },
);
this.marks.push(range);
}
resolveMarks(): ResolvedMark[] {
if (!this.text || !this.marks) {
throw new Error(
"Cannot resolve ranges without loaded text and ranges",
);
}
const ranges = this.marks.flatMap((mark) => {
if (!mark) return [];
const startBefore = this.idxAfter(mark.startBefore);
const endAfter = this.idxAfter(mark.endAfter);
if (
startBefore === undefined ||
endAfter === undefined
) {
return [];
}
const startAfter = mark.startAfter ? this.idxAfter(mark.startAfter) : startBefore - 1;
const endBefore = mark.endBefore ? this.idxAfter(mark.endBefore) : endAfter + 1;
if (
startAfter === undefined ||
endBefore === undefined
) {
return [];
}
return [
{
sourceMark: mark,
startAfter,
startBefore,
endAfter,
endBefore,
tag: mark.tag,
from: mark,
},
];
});
return ranges;
}
resolveAndDiffuseMarks(): ResolvedAndDiffusedMark[] {
return this.resolveMarks().flatMap((range) => [
...(range.startAfter < range.startBefore - 1
? [
{
start: range.startAfter,
end: range.startBefore - 1,
side: "uncertainStart" as const,
sourceMark: range.sourceMark,
},
]
: []),
{
start: range.startBefore - 1,
end: range.endAfter + 1,
side: "certainMiddle" as const,
sourceMark: range.sourceMark,
},
...(range.endAfter + 1 < range.endBefore
? [
{
start: range.endAfter + 1,
end: range.endBefore,
side: "uncertainEnd" as const,
sourceMark: range.sourceMark,
},
]
: []),
]);
}
resolveAndDiffuseAndFocusMarks(): ResolvedAndFocusedMark[] {
// for now we only keep the certainMiddle ranges
return this.resolveAndDiffuseMarks().filter(
(range) => range.side === "certainMiddle",
);
}
toTree(tagPrecedence: string[]): TreeNode {
const ranges = this.resolveAndDiffuseAndFocusMarks();
// convert a bunch of (potentially overlapping) ranges into a tree
// - make sure we include all text in leaves, even if it's not covered by a range
// - we split overlapping ranges in a way where the higher precedence (tag earlier in tagPrecedence)
// stays intact and the lower precende tag is split into two ranges, one inside and one outside the higher precedence range
const text = this.text?.toString() || "";
let currentNodes: (TreeLeaf | TreeNode)[] = [
{
type: "leaf",
start: 0,
end: text.length,
},
];
const rangesSortedLowToHighPrecedence = ranges.sort((a, b) => {
const aPrecedence = tagPrecedence.indexOf(a.sourceMark.tag);
const bPrecedence = tagPrecedence.indexOf(b.sourceMark.tag);
return bPrecedence - aPrecedence;
});
// for each range, split the current nodes where necessary (no matter if leaf or already a node), wrapping the resulting "inside" parts in a node with the range's tag
for (const range of rangesSortedLowToHighPrecedence) {
// console.log("currentNodes", currentNodes);
const newNodes = currentNodes.flatMap((node) => {
const [before, inOrAfter] = splitNode(node, range.start);
const [inside, after] = inOrAfter
? splitNode(inOrAfter, range.end)
: [undefined, undefined];
// console.log("split", range.start, range.end, {
// before,
// inside,
// after,
// });
// TODO: also split children
return [
...(before ? [before] : []),
...(inside
? [
{
type: "node" as const,
tag: range.sourceMark.tag,
start: inside.start,
end: inside.end,
children: [inside],
},
]
: []),
...(after ? [after] : []),
];
});
currentNodes = newNodes;
}
return {
type: "node",
tag: "root",
start: 0,
end: text.length,
children: currentNodes,
};
}
toString() {
if (!this.text) return "";
return this.text.toString();
}
}
export type TreeLeaf = {
type: "leaf";
start: number;
end: number;
};
export type TreeNode = {
type: "node";
tag: string;
start: number;
end: number;
range?: ResolvedAndFocusedMark;
children: (TreeNode | TreeLeaf)[];
};
function splitNode(
node: TreeNode | TreeLeaf,
at: number,
): [TreeNode | TreeLeaf | undefined, TreeNode | TreeLeaf | undefined] {
if (node.type === "leaf") {
return [
at > node.start
? {
type: "leaf",
start: node.start,
end: Math.min(at, node.end),
}
: undefined,
at < node.end
? {
type: "leaf",
start: Math.max(at, node.start),
end: node.end,
}
: undefined,
];
} else {
const children = node.children;
return [
at > node.start
? {
type: "node",
tag: node.tag,
start: node.start,
end: Math.min(at, node.end),
children: children
.map((child) => splitNode(child, at)[0])
.filter(
(c): c is Exclude<typeof c, undefined> => !!c,
),
}
: undefined,
at < node.end
? {
type: "node",
tag: node.tag,
start: Math.max(at, node.start),
end: node.end,
children: children
.map((child) => splitNode(child, at)[1])
.filter(
(c): c is Exclude<typeof c, undefined> => !!c,
),
}
: undefined,
];
}
}
export const Marks = {
Heading: class Heading extends Mark {
tag = co.literal("heading");
level = co.number;
},
Paragraph: class Paragraph extends Mark {
tag = co.literal("paragraph");
},
Link: class Link extends Mark {
tag = co.literal("link");
url = co.string;
},
Strong: class Strong extends Mark {
tag = co.literal("strong");
},
Em: class Italic extends Mark {
tag = co.literal("em");
},
};

View File

@@ -81,7 +81,10 @@ export function fulfillsDepth(depth: any, value: CoValue): boolean {
).optional,
);
}
} else if (value._type === "BinaryCoStream") {
} else if (
value._type === "BinaryCoStream" ||
value._type === "CoPlainText"
) {
return true;
} else {
console.error(value);
@@ -226,4 +229,10 @@ export type DeeplyLoaded<
},
]
? V
: never;
: [V] extends [
{
_type: "CoPlainText";
},
]
? V
: never;

View File

@@ -17,6 +17,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> {
// 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 +78,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>;
@@ -131,6 +168,10 @@ export type EffectSchemaWithInputAndOutput<A, I = A> = EffectSchema<
};
export type Encoder<V> = EffectSchemaWithInputAndOutput<V, JsonValue>;
export type OptionalEncoder<V> = EffectSchemaWithInputAndOutput<
V,
JsonValue | undefined
>;
import { Date } from "@effect/schema/Schema";
import { SchemaInit, ItemsSym, MembersSym } from "./symbols.js";

View File

@@ -15,70 +15,6 @@ export const subscriptionsScopes = new WeakMap<
const TRACE_INVALIDATIONS = false;
let nSeconds = 1;
setInterval(() => console.log(nSeconds++ + "s passed"), 1000);
export interface ThrottleOptions {
/**
* Fire immediately on the first call.
*/
start?: boolean;
/**
* Fire as soon as `wait` has passed.
*/
middle?: boolean;
/**
* Cancel after the first successful call.
*/
once?: boolean;
}
interface Throttler<T extends unknown[]> {
(...args: T): void;
cancel(): void;
}
export function throttle<T extends unknown[]>(
callback: (...args: T) => unknown,
wait = 0,
{ start = true, middle = true, once = false }: ThrottleOptions = {},
): Throttler<T> {
let innerStart = start;
let last = 0;
let timer: ReturnType<typeof setTimeout>;
let cancelled = false;
function fn(this: unknown, ...args: T) {
if (cancelled) return;
const delta = Date.now() - last;
last = Date.now();
if (start && middle && delta >= wait) {
innerStart = true;
}
if (innerStart) {
innerStart = false;
callback.apply(this, args);
if (once) fn.cancel();
} else if ((middle && delta < wait) || !middle) {
clearTimeout(timer);
timer = setTimeout(
() => {
last = Date.now();
callback.apply(this, args);
if (once) fn.cancel();
},
!middle ? wait : wait - delta,
);
}
}
fn.cancel = () => {
clearTimeout(timer);
cancelled = true;
};
return fn;
}
export class SubscriptionScope<Root extends CoValue> {
scopeID: string = `scope-${Math.random().toString(36).slice(2)}`;
subscriber: Account;
@@ -112,11 +48,7 @@ export class SubscriptionScope<Root extends CoValue> {
subscriptionsScopes.set(root, this);
this.subscriber = root._loadedAs;
this.onUpdate = throttle((update) => {
console.log("onUpdate");
onUpdate(update);
}, 50);
this.onUpdate = onUpdate;
this.rootEntry.rawUnsub = root._raw.core.subscribe(
(rawUpdate: RawCoValue | undefined) => {
if (!rawUpdate) return;

View File

@@ -19,6 +19,8 @@ export { Encoders, co } from "./internal.js";
export { CoMap, type CoMapInit } from "./internal.js";
export { CoList } from "./internal.js";
export { CoPlainText, TextPos } from "./internal.js";
export { CoRichText, Mark, Marks, TreeNode, TreeLeaf } from "./internal.js";
export { CoStream, BinaryCoStream } from "./internal.js";
export { Group, Profile } from "./internal.js";
export { Account, isControlledAccount } from "./internal.js";

View File

@@ -5,6 +5,8 @@ export * from "./coValues/interfaces.js";
export * from "./coValues/coMap.js";
export * from "./coValues/account.js";
export * from "./coValues/coList.js";
export * from "./coValues/coPlainText.js";
export * from "./coValues/coRichText.js";
export * from "./coValues/coStream.js";
export * from "./coValues/group.js";

Some files were not shown because too many files have changed in this diff Show More