Merge remote-tracking branch 'jazz-react/main'
This commit is contained in:
18
.eslintrc.cjs
Normal file
18
.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
root: true,
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
},
|
||||
|
||||
};
|
||||
171
.gitignore
vendored
Normal file
171
.gitignore
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
\*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
\*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
\*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
\*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
.cache/
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.\*
|
||||
|
||||
.DS_Store
|
||||
17
package.json
Normal file
17
package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "jazz-react",
|
||||
"version": "0.0.6",
|
||||
"main": "src/index.tsx",
|
||||
"types": "src/index.tsx",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.0.14",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.19"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2"
|
||||
}
|
||||
}
|
||||
291
src/index.tsx
Normal file
291
src/index.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
import {
|
||||
LocalNode,
|
||||
internals as cojsonInternals,
|
||||
SessionID,
|
||||
ContentType,
|
||||
SyncMessage,
|
||||
AgentSecret,
|
||||
} from "cojson";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ReadableStream, WritableStream } from "isomorphic-streams";
|
||||
import { CoID } from "cojson";
|
||||
import { AgentID } from "cojson/src/ids";
|
||||
import { getAgentID } from "cojson/src/crypto";
|
||||
import { AnonymousControlledAccount } from "cojson/src/account";
|
||||
import { IDBStorage } from "jazz-storage-indexeddb";
|
||||
|
||||
type JazzContext = {
|
||||
localNode: LocalNode;
|
||||
};
|
||||
|
||||
const JazzContext = React.createContext<JazzContext | undefined>(undefined);
|
||||
|
||||
export type AuthComponent = (props: {
|
||||
onCredential: (credentials: AgentSecret) => void;
|
||||
}) => React.ReactElement;
|
||||
|
||||
export function WithJazz({
|
||||
children,
|
||||
auth: Auth,
|
||||
syncAddress = "wss://sync.jazz.tools",
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
auth: AuthComponent;
|
||||
syncAddress?: string;
|
||||
}) {
|
||||
const [node, setNode] = useState<LocalNode | undefined>();
|
||||
const sessionDone = useRef<() => void>();
|
||||
|
||||
const onCredential = useCallback((credential: AgentSecret) => {
|
||||
const agentID = getAgentID(
|
||||
credential
|
||||
);
|
||||
const sessionHandle = getSessionFor(agentID);
|
||||
|
||||
sessionHandle.session.then((sessionID) =>
|
||||
setNode(new LocalNode(new AnonymousControlledAccount(credential), sessionID))
|
||||
);
|
||||
|
||||
sessionDone.current = sessionHandle.done;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
sessionDone.current && sessionDone.current();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (node) {
|
||||
IDBStorage.connectTo(node, {trace: true})
|
||||
|
||||
let shouldTryToReconnect = true;
|
||||
let ws: WebSocket | undefined;
|
||||
|
||||
(async function websocketReconnectLoop() {
|
||||
while (shouldTryToReconnect) {
|
||||
ws = new WebSocket(syncAddress);
|
||||
|
||||
const timeToReconnect = new Promise<void>((resolve) => {
|
||||
if (
|
||||
!ws ||
|
||||
ws.readyState === WebSocket.CLOSING ||
|
||||
ws.readyState === WebSocket.CLOSED
|
||||
)
|
||||
resolve();
|
||||
ws?.addEventListener(
|
||||
"close",
|
||||
() => {
|
||||
console.log(
|
||||
"Connection closed, reconnecting in 5s"
|
||||
);
|
||||
setTimeout(resolve, 5000);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
|
||||
const incoming = websocketReadableStream<SyncMessage>(ws);
|
||||
const outgoing = websocketWritableStream<SyncMessage>(ws);
|
||||
|
||||
node.sync.addPeer({
|
||||
id: syncAddress + "@" + new Date().toISOString(),
|
||||
incoming,
|
||||
outgoing,
|
||||
role: "server",
|
||||
});
|
||||
|
||||
await timeToReconnect;
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
shouldTryToReconnect = false;
|
||||
ws?.close();
|
||||
};
|
||||
}
|
||||
}, [node, syncAddress]);
|
||||
|
||||
return node ? (
|
||||
<JazzContext.Provider value={{ localNode: node }}>
|
||||
<>{children}</>
|
||||
</JazzContext.Provider>
|
||||
) : (
|
||||
<Auth onCredential={onCredential} />
|
||||
);
|
||||
}
|
||||
|
||||
type SessionHandle = {
|
||||
session: Promise<SessionID>;
|
||||
done: () => void;
|
||||
};
|
||||
|
||||
function getSessionFor(agentID: AgentID): SessionHandle {
|
||||
let done!: () => void;
|
||||
const donePromise = new Promise<void>((resolve) => {
|
||||
done = resolve;
|
||||
});
|
||||
|
||||
let resolveSession: (sessionID: SessionID) => void;
|
||||
const sessionPromise = new Promise<SessionID>((resolve) => {
|
||||
resolveSession = resolve;
|
||||
});
|
||||
|
||||
(async function () {
|
||||
for (let idx = 0; idx < 100; idx++) {
|
||||
// To work better around StrictMode
|
||||
for (let retry = 0; retry < 2; retry++) {
|
||||
console.log("Trying to get lock", agentID + "_" + idx);
|
||||
const sessionFinishedOrNoLock = await navigator.locks.request(
|
||||
agentID + "_" + idx,
|
||||
{ ifAvailable: true },
|
||||
async (lock) => {
|
||||
if (!lock) return "noLock";
|
||||
|
||||
const sessionID =
|
||||
localStorage[agentID + "_" + idx] ||
|
||||
cojsonInternals.newRandomSessionID(agentID);
|
||||
localStorage[agentID + "_" + idx] = sessionID;
|
||||
|
||||
console.log("Got lock", agentID + "_" + idx, sessionID);
|
||||
|
||||
resolveSession(sessionID);
|
||||
|
||||
await donePromise;
|
||||
console.log(
|
||||
"Done with lock",
|
||||
agentID + "_" + idx,
|
||||
sessionID
|
||||
);
|
||||
return "sessionFinished";
|
||||
}
|
||||
);
|
||||
|
||||
if (sessionFinishedOrNoLock === "sessionFinished") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error("Couldn't get lock on session after 100x2 tries");
|
||||
})();
|
||||
|
||||
return {
|
||||
session: sessionPromise,
|
||||
done,
|
||||
};
|
||||
}
|
||||
|
||||
export function useJazz() {
|
||||
const context = React.useContext(JazzContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useJazz must be used within a WithJazz provider");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useTelepathicState<T extends ContentType>(id: CoID<T>) {
|
||||
const [state, setState] = useState<T>();
|
||||
|
||||
const { localNode } = useJazz();
|
||||
|
||||
useEffect(() => {
|
||||
let unsubscribe: (() => void) | undefined = undefined;
|
||||
|
||||
let done = false;
|
||||
|
||||
localNode.load(id).then((state) => {
|
||||
if (done) return;
|
||||
unsubscribe = state.subscribe((newState) => {
|
||||
console.log(
|
||||
"Got update",
|
||||
id,
|
||||
newState.toJSON(),
|
||||
newState.coValue.sessions
|
||||
);
|
||||
setState(newState as T);
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
done = true;
|
||||
unsubscribe && unsubscribe();
|
||||
};
|
||||
}, [localNode, id]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function websocketReadableStream<T>(ws: WebSocket) {
|
||||
ws.binaryType = "arraybuffer";
|
||||
|
||||
return new ReadableStream<T>({
|
||||
start(controller) {
|
||||
ws.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === "ping") {
|
||||
console.debug(
|
||||
"Got ping from",
|
||||
msg.dc,
|
||||
"latency",
|
||||
Date.now() - msg.time,
|
||||
"ms"
|
||||
);
|
||||
return;
|
||||
}
|
||||
controller.enqueue(msg);
|
||||
};
|
||||
ws.onclose = () => controller.close();
|
||||
ws.onerror = () =>
|
||||
controller.error(new Error("The WebSocket errored!"));
|
||||
},
|
||||
|
||||
cancel() {
|
||||
ws.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function websocketWritableStream<T>(ws: WebSocket) {
|
||||
return new WritableStream<T>({
|
||||
start(controller) {
|
||||
ws.onerror = () => {
|
||||
controller.error(new Error("The WebSocket errored!"));
|
||||
ws.onclose = null;
|
||||
};
|
||||
ws.onclose = () =>
|
||||
controller.error(
|
||||
new Error("The server closed the connection unexpectedly!")
|
||||
);
|
||||
return new Promise((resolve) => (ws.onopen = resolve));
|
||||
},
|
||||
|
||||
write(chunk) {
|
||||
ws.send(JSON.stringify(chunk));
|
||||
// Return immediately, since the web socket gives us no easy way to tell
|
||||
// when the write completes.
|
||||
},
|
||||
|
||||
close() {
|
||||
return closeWS(1000);
|
||||
},
|
||||
|
||||
abort(reason) {
|
||||
return closeWS(4000, reason && reason.message);
|
||||
},
|
||||
});
|
||||
|
||||
function closeWS(code: number, reasonString?: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
ws.onclose = (e) => {
|
||||
if (e.wasClean) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error("The connection was not closed cleanly"));
|
||||
}
|
||||
};
|
||||
ws.close(code, reasonString);
|
||||
});
|
||||
}
|
||||
}
|
||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"allowImportingTsExtensions": true,
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "preserve",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"esModuleInterop": true,
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
}
|
||||
75
yarn.lock
Normal file
75
yarn.lock
Normal file
@@ -0,0 +1,75 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@noble/ciphers@^0.1.3":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.1.4.tgz#96327dca147829ed9eee0d96cfdf7c57915765f0"
|
||||
integrity sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==
|
||||
|
||||
"@noble/curves@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d"
|
||||
integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==
|
||||
dependencies:
|
||||
"@noble/hashes" "1.3.1"
|
||||
|
||||
"@noble/hashes@1.3.1", "@noble/hashes@^1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
|
||||
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
|
||||
|
||||
"@scure/base@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
|
||||
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
||||
|
||||
"@types/react@^18.2.19":
|
||||
version "18.2.19"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.19.tgz#f77cb2c8307368e624d464a25b9675fa35f95a8b"
|
||||
integrity sha512-e2S8wmY1ePfM517PqCG80CcE48Xs5k0pwJzuDZsfE8IZRRBfOMCF+XqnFxu6mWtyivum1MQm4aco+WIt6Coimw==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/scheduler@*":
|
||||
version "0.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
||||
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
|
||||
|
||||
cojson@^0.0.14:
|
||||
version "0.0.14"
|
||||
resolved "https://registry.yarnpkg.com/cojson/-/cojson-0.0.14.tgz#e7b190ade1efc20d6f0fa12411d7208cdc0f19f7"
|
||||
integrity sha512-TFenIGswEEhnZlCmq+B1NZPztjovZ72AjK1YkkZca54ZFbB1lAHdPt2hqqu/QBO24C9+6DtuoS2ixm6gbSBWCg==
|
||||
dependencies:
|
||||
"@noble/ciphers" "^0.1.3"
|
||||
"@noble/curves" "^1.1.0"
|
||||
"@noble/hashes" "^1.3.1"
|
||||
"@scure/base" "^1.1.1"
|
||||
fast-json-stable-stringify "^2.1.0"
|
||||
isomorphic-streams "https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
|
||||
|
||||
csstype@^3.0.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||
|
||||
fast-json-stable-stringify@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
||||
|
||||
"isomorphic-streams@git+https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae":
|
||||
version "1.0.3"
|
||||
resolved "git+https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
|
||||
|
||||
typescript@^5.1.6:
|
||||
version "5.1.6"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
|
||||
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
|
||||
Reference in New Issue
Block a user