Compare commits
19 Commits
cojson@0.1
...
jazz-react
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9486a82c3 | ||
|
|
d0babab822 | ||
|
|
ab34172e01 | ||
|
|
b779a91611 | ||
|
|
297a8646dd | ||
|
|
25eb3e097f | ||
|
|
29abbc455c | ||
|
|
f6864e0f93 | ||
|
|
9440b5306c | ||
|
|
aa34f1e8a6 | ||
|
|
24ce7dbdf1 | ||
|
|
65a7a66c15 | ||
|
|
0f999a2c2d | ||
|
|
2247c97080 | ||
|
|
cbdc722959 | ||
|
|
bb157b6099 | ||
|
|
e1f8ec6f11 | ||
|
|
9854238346 | ||
|
|
3b5ab90006 |
10
.github/workflows/build-and-deploy.yaml
vendored
10
.github/workflows/build-and-deploy.yaml
vendored
@@ -71,11 +71,7 @@ jobs:
|
||||
export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
|
||||
export DOCKER_TAG=${{ env.DOCKER_TAG }};
|
||||
|
||||
for region in ${{ vars.DEPLOY_REGIONS }}
|
||||
do
|
||||
export REGION=$region;
|
||||
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN} ${REGION}' < job-template.nomad > job-instance.nomad;
|
||||
cat job-instance.nomad;
|
||||
NOMAD_ADDR='${{ secrets.NOMAD_ADDR }}' nomad job run job-instance.nomad;
|
||||
done
|
||||
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
|
||||
cat job-instance.nomad;
|
||||
NOMAD_ADDR='http://control1v2-london:4646' nomad job run job-instance.nomad;
|
||||
working-directory: ./examples/todo
|
||||
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
job "example-todo$BRANCH_SUFFIX" {
|
||||
region = "$REGION"
|
||||
datacenters = ["$REGION"]
|
||||
region = "global"
|
||||
datacenters = ["*"]
|
||||
|
||||
group "static" {
|
||||
// count = 3
|
||||
count = 8
|
||||
|
||||
network {
|
||||
port "http" {
|
||||
@@ -14,13 +14,17 @@ job "example-todo$BRANCH_SUFFIX" {
|
||||
constraint {
|
||||
attribute = "${node.class}"
|
||||
operator = "="
|
||||
value = "edge"
|
||||
value = "mesh"
|
||||
}
|
||||
|
||||
// spread {
|
||||
// attribute = "${node.datacenter}"
|
||||
// weight = 100
|
||||
// }
|
||||
spread {
|
||||
attribute = "${node.datacenter}"
|
||||
weight = 100
|
||||
}
|
||||
|
||||
constraint {
|
||||
distinct_hosts = true
|
||||
}
|
||||
|
||||
task "server" {
|
||||
driver = "docker"
|
||||
@@ -37,9 +41,7 @@ job "example-todo$BRANCH_SUFFIX" {
|
||||
|
||||
service {
|
||||
tags = ["public"]
|
||||
meta {
|
||||
public_name = "${BRANCH_SUBDOMAIN}example-todo"
|
||||
}
|
||||
name = "example-todo$BRANCH_SUFFIX"
|
||||
port = "http"
|
||||
provider = "consul"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.16",
|
||||
"version": "0.0.21",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -13,11 +13,13 @@
|
||||
"@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",
|
||||
"jazz-react": "^0.1.3",
|
||||
"jazz-react-auth-local": "^0.1.3",
|
||||
"jazz-react": "^0.1.7",
|
||||
"jazz-react-auth-local": "^0.1.7",
|
||||
"lucide-react": "^0.265.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
|
||||
@@ -6,28 +6,34 @@ import {
|
||||
useJazz,
|
||||
useProfile,
|
||||
useTelepathicState,
|
||||
createInviteLink
|
||||
createInviteLink,
|
||||
} from "jazz-react";
|
||||
|
||||
import { SubmittableInput } from "./components/SubmittableInput";
|
||||
import { useToast } from "./components/ui/use-toast";
|
||||
import { Skeleton } from "./components/ui/skeleton";
|
||||
import {
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import uniqolor from "uniqolor";
|
||||
import QRCode from "qrcode";
|
||||
import { CoList } from "cojson/dist/contentTypes/coList";
|
||||
|
||||
type TaskContent = { done: boolean; text: string };
|
||||
type Task = CoMap<TaskContent>;
|
||||
type Task = CoMap<{ done: boolean; text: string }>;
|
||||
|
||||
type TodoListContent = {
|
||||
type ListOfTasks = CoList<CoID<Task>>;
|
||||
|
||||
type TodoList = CoMap<{
|
||||
title: string;
|
||||
// other keys form a set of task IDs
|
||||
[taskId: CoID<Task>]: true;
|
||||
};
|
||||
type TodoList = CoMap<TodoListContent>;
|
||||
tasks: CoID<ListOfTasks>;
|
||||
}>;
|
||||
|
||||
export default function App() {
|
||||
const [listId, setListId] = useState<CoID<TodoList>>();
|
||||
@@ -57,11 +63,14 @@ export default function App() {
|
||||
|
||||
const createList = useCallback(
|
||||
(title: string) => {
|
||||
if (!title) return;
|
||||
const listGroup = localNode.createGroup();
|
||||
const list = listGroup.createMap<TodoListContent>();
|
||||
const list = listGroup.createMap<TodoList>();
|
||||
const tasks = listGroup.createList<ListOfTasks>();
|
||||
|
||||
list.edit((list) => {
|
||||
list.set("title", title);
|
||||
list.set("tasks", tasks.id);
|
||||
});
|
||||
|
||||
window.location.hash = list.id;
|
||||
@@ -95,18 +104,19 @@ export default function App() {
|
||||
|
||||
export function TodoListComponent({ listId }: { listId: CoID<TodoList> }) {
|
||||
const list = useTelepathicState(listId);
|
||||
const tasks = useTelepathicState(list?.get("tasks"));
|
||||
|
||||
const createTask = (text: string) => {
|
||||
if (!list) return;
|
||||
const task = list.coValue.getGroup().createMap<TaskContent>();
|
||||
if (!tasks || !text) return;
|
||||
const task = tasks.coValue.getGroup().createMap<Task>();
|
||||
|
||||
task.edit((task) => {
|
||||
task.set("text", text);
|
||||
task.set("done", false);
|
||||
});
|
||||
|
||||
list.edit((list) => {
|
||||
list.set(task.id, true);
|
||||
tasks.edit((tasks) => {
|
||||
tasks.push(task.id);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -133,12 +143,9 @@ export function TodoListComponent({ listId }: { listId: CoID<TodoList> }) {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{list &&
|
||||
list
|
||||
.keys()
|
||||
.filter((key): key is CoID<Task> =>
|
||||
key.startsWith("co_")
|
||||
)
|
||||
{tasks &&
|
||||
tasks
|
||||
.asArray()
|
||||
.map((taskId) => (
|
||||
<TaskRow key={taskId} taskId={taskId} />
|
||||
))}
|
||||
@@ -180,7 +187,9 @@ function TaskRow({ taskId }: { taskId: CoID<Task> }) {
|
||||
<TableCell>
|
||||
<div className="flex flex-row justify-between items-center gap-2">
|
||||
<span className={task?.get("done") ? "line-through" : ""}>
|
||||
{task?.get("text") || <Skeleton className="mt-1 w-[200px] h-[1em] rounded-full" />}
|
||||
{task?.get("text") || (
|
||||
<Skeleton className="mt-1 w-[200px] h-[1em] rounded-full" />
|
||||
)}
|
||||
</span>
|
||||
<NameBadge accountID={task?.getLastEditor("text")} />
|
||||
</div>
|
||||
@@ -200,15 +209,19 @@ function NameBadge({ accountID }: { accountID?: AccountID }) {
|
||||
const darkColor = uniqolor(accountID || "", { lightness: 20 }).color;
|
||||
|
||||
return (
|
||||
profile?.get("name") && <span
|
||||
className="rounded-full py-0.5 px-2 text-xs"
|
||||
style={{
|
||||
color: theme == "light" ? darkColor : brightColor,
|
||||
background: theme == "light" ? brightColor : darkColor,
|
||||
}}
|
||||
>
|
||||
{profile.get("name")}
|
||||
</span>
|
||||
profile?.get("name") ? (
|
||||
<span
|
||||
className="rounded-full py-0.5 px-2 text-xs"
|
||||
style={{
|
||||
color: theme == "light" ? darkColor : brightColor,
|
||||
background: theme == "light" ? brightColor : darkColor,
|
||||
}}
|
||||
>
|
||||
{profile.get("name")}
|
||||
</span>
|
||||
) : (
|
||||
<Skeleton className="mt-1 w-[50px] h-[1em] rounded-full" />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -223,16 +236,22 @@ function InviteButton({ list }: { list: TodoList }) {
|
||||
className="py-0"
|
||||
disabled={!list}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
let inviteLink = existingInviteLink;
|
||||
if (list && !inviteLink) {
|
||||
inviteLink = createInviteLink(list, "writer");
|
||||
setExistingInviteLink(inviteLink);
|
||||
}
|
||||
if (inviteLink) {
|
||||
const qr = await QRCode.toDataURL(inviteLink, {
|
||||
errorCorrectionLevel: "L",
|
||||
});
|
||||
navigator.clipboard.writeText(inviteLink).then(() =>
|
||||
toast({
|
||||
description: "Copied invite link to clipboard!",
|
||||
title: "Copied invite link to clipboard!",
|
||||
description: (
|
||||
<img src={qr} className="w-20 h-20" />
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -242,4 +261,4 @@ function InviteButton({ list }: { list: TodoList }) {
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
// <React.StrictMode>
|
||||
<ThemeProvider>
|
||||
<div className="flex items-center gap-2 justify-center mt-5">
|
||||
<img src="jazz-logo.png" className="h-5" /> Jazz Todo List
|
||||
@@ -31,5 +31,5 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<Toaster />
|
||||
</WithJazz>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
// </React.StrictMode>
|
||||
);
|
||||
|
||||
3
packages/cojson-simple-sync/.gitignore
vendored
3
packages/cojson-simple-sync/.gitignore
vendored
@@ -170,4 +170,5 @@ dist
|
||||
|
||||
.DS_Store
|
||||
|
||||
out
|
||||
out
|
||||
sync.db*
|
||||
@@ -4,7 +4,7 @@
|
||||
"types": "src/index.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.7",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/ws": "^8.5.5",
|
||||
@@ -16,7 +16,8 @@
|
||||
"typescript": "5.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"cojson": "^0.1.3",
|
||||
"cojson": "^0.1.6",
|
||||
"cojson-storage-sqlite": "^0.1.4",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { AnonymousControlledAccount, LocalNode, cojsonInternals } from "cojson";
|
||||
import { WebSocketServer, createWebSocketStream } from "ws";
|
||||
import { Duplex } from "node:stream";
|
||||
import { TransformStream } from "node:stream/web"
|
||||
import { WebSocketServer } from "ws";
|
||||
import { SQLiteStorage } from "cojson-storage-sqlite";
|
||||
import { websocketReadableStream, websocketWritableStream } from "./websocketStreams.js";
|
||||
|
||||
const wss = new WebSocketServer({ port: 4200 });
|
||||
|
||||
console.log("COJSON sync server listening on port " + wss.options.port)
|
||||
console.log("COJSON sync server listening on port " + wss.options.port);
|
||||
|
||||
const agentSecret = cojsonInternals.newRandomAgentSecret();
|
||||
const agentID = cojsonInternals.getAgentID(agentSecret);
|
||||
@@ -15,28 +15,26 @@ const localNode = new LocalNode(
|
||||
cojsonInternals.newRandomSessionID(agentID)
|
||||
);
|
||||
|
||||
SQLiteStorage.asPeer({ filename: "./sync.db" })
|
||||
.then((storage) => localNode.sync.addPeer(storage))
|
||||
.catch((e) => console.error(e));
|
||||
|
||||
wss.on("connection", function connection(ws, req) {
|
||||
const duplexStream = createWebSocketStream(ws, {
|
||||
decodeStrings: false,
|
||||
readableObjectMode: true,
|
||||
writableObjectMode: true,
|
||||
encoding: "utf-8",
|
||||
defaultEncoding: "utf-8",
|
||||
const pinging = setInterval(() => {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "ping",
|
||||
time: Date.now(),
|
||||
dc: "cojson-simple-sync",
|
||||
})
|
||||
);
|
||||
}, 2000);
|
||||
|
||||
ws.on("close", () => {
|
||||
clearInterval(pinging);
|
||||
});
|
||||
|
||||
const { readable: incomingStrings, writable: outgoingStrings } = Duplex.toWeb(duplexStream);
|
||||
|
||||
const toJSON = new TransformStream({
|
||||
transform: (chunk, controller) => {
|
||||
controller.enqueue(JSON.parse(chunk));
|
||||
}
|
||||
})
|
||||
|
||||
const fromJSON = new TransformStream({
|
||||
transform: (chunk, controller) => {
|
||||
controller.enqueue(JSON.stringify(chunk));
|
||||
}
|
||||
});
|
||||
|
||||
const clientAddress =
|
||||
(req.headers["x-forwarded-for"] as string | undefined)
|
||||
@@ -48,11 +46,9 @@ wss.on("connection", function connection(ws, req) {
|
||||
localNode.sync.addPeer({
|
||||
id: clientId,
|
||||
role: "client",
|
||||
incoming: incomingStrings.pipeThrough(toJSON),
|
||||
outgoing: fromJSON.writable,
|
||||
incoming: websocketReadableStream(ws),
|
||||
outgoing: websocketWritableStream(ws),
|
||||
});
|
||||
|
||||
void fromJSON.readable.pipeTo(outgoingStrings);
|
||||
|
||||
ws.on("error", (e) => console.error(`Error on connection ${clientId}:`, e));
|
||||
});
|
||||
|
||||
86
packages/cojson-simple-sync/src/websocketStreams.ts
Normal file
86
packages/cojson-simple-sync/src/websocketStreams.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { WebSocket } from "ws";
|
||||
import { WritableStream, ReadableStream } from "isomorphic-streams";
|
||||
|
||||
export function websocketReadableStream<T>(ws: WebSocket) {
|
||||
ws.binaryType = "arraybuffer";
|
||||
|
||||
return new ReadableStream<T>({
|
||||
start(controller) {
|
||||
ws.addEventListener("message", (event) => {
|
||||
if (typeof event.data !== "string")
|
||||
return console.warn(
|
||||
"Got non-string message from client",
|
||||
event.data
|
||||
);
|
||||
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.addEventListener("close", () => controller.close());
|
||||
ws.addEventListener("error", () =>
|
||||
controller.error(new Error("The WebSocket errored!"))
|
||||
);
|
||||
},
|
||||
|
||||
cancel() {
|
||||
ws.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function websocketWritableStream<T>(ws: WebSocket) {
|
||||
return new WritableStream<T>({
|
||||
start(controller) {
|
||||
ws.addEventListener("close", () =>
|
||||
controller.error(
|
||||
new Error("The WebSocket closed unexpectedly!")
|
||||
)
|
||||
);
|
||||
ws.addEventListener("error", () =>
|
||||
controller.error(new Error("The WebSocket errored!"))
|
||||
);
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => ws.once("open", 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
17
packages/cojson-storage-sqlite/.eslintrc.cjs
Normal file
17
packages/cojson-storage-sqlite/.eslintrc.cjs
Normal file
@@ -0,0 +1,17 @@
|
||||
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
packages/cojson-storage-sqlite/.gitignore
vendored
Normal file
171
packages/cojson-storage-sqlite/.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
|
||||
2
packages/cojson-storage-sqlite/.npmignore
Normal file
2
packages/cojson-storage-sqlite/.npmignore
Normal file
@@ -0,0 +1,2 @@
|
||||
coverage
|
||||
node_modules
|
||||
21
packages/cojson-storage-sqlite/package.json
Normal file
21
packages/cojson-storage-sqlite/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "cojson-storage-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.1.4",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^8.5.2",
|
||||
"cojson": "^0.1.6",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.4"
|
||||
}
|
||||
}
|
||||
386
packages/cojson-storage-sqlite/src/index.ts
Normal file
386
packages/cojson-storage-sqlite/src/index.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
import {
|
||||
cojsonInternals,
|
||||
SyncMessage,
|
||||
Peer,
|
||||
CojsonInternalTypes,
|
||||
SessionID,
|
||||
// CojsonInternalTypes,
|
||||
// SessionID,
|
||||
} from "cojson";
|
||||
import {
|
||||
ReadableStream,
|
||||
WritableStream,
|
||||
ReadableStreamDefaultReader,
|
||||
WritableStreamDefaultWriter,
|
||||
} from "isomorphic-streams";
|
||||
|
||||
import Database, { Database as DatabaseT } from "better-sqlite3";
|
||||
import { RawCoID } from "cojson/dist/ids";
|
||||
|
||||
type CoValueRow = {
|
||||
id: CojsonInternalTypes.RawCoID;
|
||||
header: string;
|
||||
};
|
||||
|
||||
type StoredCoValueRow = CoValueRow & { rowID: number };
|
||||
|
||||
type SessionRow = {
|
||||
coValue: number;
|
||||
sessionID: SessionID;
|
||||
lastIdx: number;
|
||||
lastSignature: CojsonInternalTypes.Signature;
|
||||
};
|
||||
|
||||
type StoredSessionRow = SessionRow & { rowID: number };
|
||||
|
||||
type TransactionRow = {
|
||||
ses: number;
|
||||
idx: number;
|
||||
tx: string;
|
||||
};
|
||||
|
||||
export class SQLiteStorage {
|
||||
fromLocalNode!: ReadableStreamDefaultReader<SyncMessage>;
|
||||
toLocalNode: WritableStreamDefaultWriter<SyncMessage>;
|
||||
db: DatabaseT;
|
||||
|
||||
constructor(
|
||||
db: DatabaseT,
|
||||
fromLocalNode: ReadableStream<SyncMessage>,
|
||||
toLocalNode: WritableStream<SyncMessage>
|
||||
) {
|
||||
this.db = db;
|
||||
this.fromLocalNode = fromLocalNode.getReader();
|
||||
this.toLocalNode = toLocalNode.getWriter();
|
||||
|
||||
(async () => {
|
||||
let done = false;
|
||||
while (!done) {
|
||||
const result = await this.fromLocalNode.read();
|
||||
done = result.done;
|
||||
|
||||
if (result.value) {
|
||||
this.handleSyncMessage(result.value);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
static async asPeer({
|
||||
filename,
|
||||
trace,
|
||||
localNodeName = "local",
|
||||
}: {
|
||||
filename: string;
|
||||
trace?: boolean;
|
||||
localNodeName?: string;
|
||||
}): Promise<Peer> {
|
||||
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
|
||||
localNodeName,
|
||||
"storage",
|
||||
{ peer1role: "client", peer2role: "server", trace }
|
||||
);
|
||||
|
||||
await SQLiteStorage.open(
|
||||
filename,
|
||||
localNodeAsPeer.incoming,
|
||||
localNodeAsPeer.outgoing
|
||||
);
|
||||
|
||||
return storageAsPeer;
|
||||
}
|
||||
|
||||
static async open(
|
||||
filename: string,
|
||||
fromLocalNode: ReadableStream<SyncMessage>,
|
||||
toLocalNode: WritableStream<SyncMessage>
|
||||
) {
|
||||
const db = Database(filename);
|
||||
db.pragma("journal_mode = WAL");
|
||||
|
||||
db.prepare(
|
||||
`CREATE TABLE IF NOT EXISTS transactions (
|
||||
ses INTEGER,
|
||||
idx INTEGER,
|
||||
tx TEXT NOT NULL ,
|
||||
PRIMARY KEY (ses, idx)
|
||||
) WITHOUT ROWID;`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`CREATE TABLE IF NOT EXISTS sessions (
|
||||
rowID INTEGER PRIMARY KEY,
|
||||
coValue INTEGER NOT NULL,
|
||||
sessionID TEXT NOT NULL,
|
||||
lastIdx INTEGER,
|
||||
lastSignature TEXT,
|
||||
UNIQUE (sessionID, coValue)
|
||||
);`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`CREATE INDEX IF NOT EXISTS sessionsByCoValue ON sessions (coValue);`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`CREATE TABLE IF NOT EXISTS coValues (
|
||||
rowID INTEGER PRIMARY KEY,
|
||||
id TEXT NOT NULL UNIQUE,
|
||||
header TEXT NOT NULL UNIQUE
|
||||
);`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`CREATE INDEX IF NOT EXISTS coValuesByID ON coValues (id);`
|
||||
).run();
|
||||
|
||||
return new SQLiteStorage(db, fromLocalNode, toLocalNode);
|
||||
}
|
||||
|
||||
async handleSyncMessage(msg: SyncMessage) {
|
||||
switch (msg.action) {
|
||||
case "load":
|
||||
await this.handleLoad(msg);
|
||||
break;
|
||||
case "content":
|
||||
await this.handleContent(msg);
|
||||
break;
|
||||
case "known":
|
||||
await this.handleKnown(msg);
|
||||
break;
|
||||
case "done":
|
||||
await this.handleDone(msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async sendNewContentAfter(
|
||||
theirKnown: CojsonInternalTypes.CoValueKnownState,
|
||||
asDependencyOf?: CojsonInternalTypes.RawCoID
|
||||
) {
|
||||
const coValueRow = (await this.db
|
||||
.prepare(`SELECT * FROM coValues WHERE id = ?`)
|
||||
.get(theirKnown.id)) as StoredCoValueRow | undefined;
|
||||
|
||||
const allOurSessions = coValueRow
|
||||
? (this.db
|
||||
.prepare<number>(`SELECT * FROM sessions WHERE coValue = ?`)
|
||||
.all(coValueRow.rowID) as StoredSessionRow[])
|
||||
: [];
|
||||
|
||||
const ourKnown: CojsonInternalTypes.CoValueKnownState = {
|
||||
id: theirKnown.id,
|
||||
header: !!coValueRow,
|
||||
sessions: {},
|
||||
};
|
||||
|
||||
const parsedHeader = (coValueRow?.header &&
|
||||
JSON.parse(coValueRow.header)) as
|
||||
| CojsonInternalTypes.CoValueHeader
|
||||
| undefined;
|
||||
|
||||
const newContent: CojsonInternalTypes.NewContentMessage = {
|
||||
action: "content",
|
||||
id: theirKnown.id,
|
||||
header: theirKnown.header ? undefined : parsedHeader,
|
||||
new: {},
|
||||
};
|
||||
|
||||
for (const sessionRow of allOurSessions) {
|
||||
ourKnown.sessions[sessionRow.sessionID] = sessionRow.lastIdx;
|
||||
|
||||
if (
|
||||
sessionRow.lastIdx >
|
||||
(theirKnown.sessions[sessionRow.sessionID] || 0)
|
||||
) {
|
||||
const firstNewTxIdx =
|
||||
theirKnown.sessions[sessionRow.sessionID] || 0;
|
||||
|
||||
const newTxInSession = this.db
|
||||
.prepare<[number, number]>(
|
||||
`SELECT * FROM transactions WHERE ses = ? AND idx > ?`
|
||||
)
|
||||
.all(sessionRow.rowID, firstNewTxIdx) as TransactionRow[];
|
||||
|
||||
newContent.new[sessionRow.sessionID] = {
|
||||
after: firstNewTxIdx,
|
||||
lastSignature: sessionRow.lastSignature,
|
||||
newTransactions: newTxInSession.map((row) =>
|
||||
JSON.parse(row.tx)
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const dependedOnCoValues =
|
||||
parsedHeader?.ruleset.type === "group"
|
||||
? Object.values(newContent.new).flatMap((sessionEntry) =>
|
||||
sessionEntry.newTransactions.flatMap((tx) => {
|
||||
if (tx.privacy !== "trusting") return [];
|
||||
return tx.changes
|
||||
.map(
|
||||
(change) =>
|
||||
change &&
|
||||
typeof change === "object" &&
|
||||
"op" in change &&
|
||||
change.op === "set" &&
|
||||
"key" in change &&
|
||||
change.key
|
||||
)
|
||||
.filter(
|
||||
(key): key is CojsonInternalTypes.RawCoID =>
|
||||
typeof key === "string" &&
|
||||
key.startsWith("co_")
|
||||
);
|
||||
})
|
||||
)
|
||||
: parsedHeader?.ruleset.type === "ownedByGroup"
|
||||
? [parsedHeader?.ruleset.group]
|
||||
: [];
|
||||
|
||||
for (const dependedOnCoValue of dependedOnCoValues) {
|
||||
await this.sendNewContentAfter(
|
||||
{ id: dependedOnCoValue, header: false, sessions: {} },
|
||||
asDependencyOf || theirKnown.id
|
||||
);
|
||||
}
|
||||
|
||||
await this.toLocalNode.write({
|
||||
action: "known",
|
||||
...ourKnown,
|
||||
asDependencyOf,
|
||||
});
|
||||
|
||||
if (newContent.header || Object.keys(newContent.new).length > 0) {
|
||||
await this.toLocalNode.write(newContent);
|
||||
}
|
||||
}
|
||||
|
||||
handleLoad(msg: CojsonInternalTypes.LoadMessage) {
|
||||
return this.sendNewContentAfter(msg);
|
||||
}
|
||||
|
||||
async handleContent(msg: CojsonInternalTypes.NewContentMessage) {
|
||||
let storedCoValueRowID = (
|
||||
this.db
|
||||
.prepare<RawCoID>(`SELECT rowID FROM coValues WHERE id = ?`)
|
||||
.get(msg.id) as StoredCoValueRow | undefined
|
||||
)?.rowID;
|
||||
|
||||
if (storedCoValueRowID === undefined) {
|
||||
const header = msg.header;
|
||||
if (!header) {
|
||||
console.error("Expected to be sent header first");
|
||||
await this.toLocalNode.write({
|
||||
action: "known",
|
||||
id: msg.id,
|
||||
header: false,
|
||||
sessions: {},
|
||||
isCorrection: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
storedCoValueRowID = this.db
|
||||
.prepare<[RawCoID, string]>(
|
||||
`INSERT INTO coValues (id, header) VALUES (?, ?)`
|
||||
)
|
||||
.run(msg.id, JSON.stringify(header)).lastInsertRowid as number;
|
||||
}
|
||||
|
||||
const ourKnown: CojsonInternalTypes.CoValueKnownState = {
|
||||
id: msg.id,
|
||||
header: true,
|
||||
sessions: {},
|
||||
};
|
||||
let invalidAssumptions = false;
|
||||
|
||||
this.db.transaction(() => {
|
||||
const allOurSessions = (
|
||||
this.db
|
||||
.prepare<number>(`SELECT * FROM sessions WHERE coValue = ?`)
|
||||
.all(storedCoValueRowID!) as StoredSessionRow[]
|
||||
).reduce((acc, row) => {
|
||||
acc[row.sessionID] = row;
|
||||
return acc;
|
||||
}, {} as { [sessionID: string]: StoredSessionRow });
|
||||
|
||||
for (const sessionID of Object.keys(msg.new) as SessionID[]) {
|
||||
const sessionRow = allOurSessions[sessionID];
|
||||
if (sessionRow) {
|
||||
ourKnown.sessions[sessionRow.sessionID] =
|
||||
sessionRow.lastIdx;
|
||||
}
|
||||
|
||||
if (
|
||||
(sessionRow?.lastIdx || 0) <
|
||||
(msg.new[sessionID]?.after || 0)
|
||||
) {
|
||||
invalidAssumptions = true;
|
||||
} else {
|
||||
const newTransactions =
|
||||
msg.new[sessionID]?.newTransactions || [];
|
||||
|
||||
const actuallyNewOffset =
|
||||
(sessionRow?.lastIdx || 0) -
|
||||
(msg.new[sessionID]?.after || 0);
|
||||
const actuallyNewTransactions =
|
||||
newTransactions.slice(actuallyNewOffset);
|
||||
|
||||
let nextIdx = sessionRow?.lastIdx || 0;
|
||||
|
||||
const sessionUpdate = {
|
||||
coValue: storedCoValueRowID!,
|
||||
sessionID: sessionID,
|
||||
lastIdx:
|
||||
(sessionRow?.lastIdx || 0) +
|
||||
actuallyNewTransactions.length,
|
||||
lastSignature: msg.new[sessionID]!.lastSignature,
|
||||
};
|
||||
|
||||
const upsertedSession = (this.db
|
||||
.prepare<[number, string, number, string]>(
|
||||
`INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature) VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(coValue, sessionID) DO UPDATE SET lastIdx=excluded.lastIdx, lastSignature=excluded.lastSignature
|
||||
RETURNING rowID`
|
||||
)
|
||||
.get(
|
||||
sessionUpdate.coValue,
|
||||
sessionUpdate.sessionID,
|
||||
sessionUpdate.lastIdx,
|
||||
sessionUpdate.lastSignature
|
||||
) as {rowID: number});
|
||||
|
||||
const sessionRowID = upsertedSession.rowID;
|
||||
|
||||
for (const newTransaction of actuallyNewTransactions) {
|
||||
nextIdx++;
|
||||
this.db
|
||||
.prepare<[number, number, string]>(
|
||||
`INSERT INTO transactions (ses, idx, tx) VALUES (?, ?, ?)`
|
||||
)
|
||||
.run(
|
||||
sessionRowID,
|
||||
nextIdx,
|
||||
JSON.stringify(newTransaction)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
if (invalidAssumptions) {
|
||||
await this.toLocalNode.write({
|
||||
action: "known",
|
||||
...ourKnown,
|
||||
isCorrection: invalidAssumptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleKnown(msg: CojsonInternalTypes.KnownStateMessage) {
|
||||
return this.sendNewContentAfter(msg);
|
||||
}
|
||||
|
||||
handleDone(_msg: CojsonInternalTypes.DoneMessage) {}
|
||||
}
|
||||
15
packages/cojson-storage-sqlite/tsconfig.json
Normal file
15
packages/cojson-storage-sqlite/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"target": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"esModuleInterop": true,
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
}
|
||||
2767
packages/cojson-storage-sqlite/yarn.lock
Normal file
2767
packages/cojson-storage-sqlite/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.6",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
|
||||
@@ -135,7 +135,8 @@ export class AnonymousControlledAccount
|
||||
|
||||
export type AccountContent = GroupContent & { profile: CoID<Profile> };
|
||||
export type AccountMeta = { type: "account" };
|
||||
export type AccountID = CoID<CoMap<AccountContent, AccountMeta>>;
|
||||
export type AccountMap = CoMap<AccountContent, AccountMeta>;
|
||||
export type AccountID = CoID<AccountMap>;
|
||||
|
||||
export type AccountIDOrAgentID = AgentID | AccountID;
|
||||
export type AccountOrAgentID = AgentID | Account;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createdNowUnique } from "./crypto.js";
|
||||
import { LocalNode } from "./node.js";
|
||||
import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
|
||||
|
||||
test("Empty COJSON Map works", () => {
|
||||
test("Empty CoMap works", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
||||
|
||||
const coValue = node.createCoValue({
|
||||
@@ -24,7 +24,7 @@ test("Empty COJSON Map works", () => {
|
||||
expect(content.toJSON()).toEqual({});
|
||||
});
|
||||
|
||||
test("Can insert and delete Map entries in edit()", () => {
|
||||
test("Can insert and delete CoMap entries in edit()", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
||||
|
||||
const coValue = node.createCoValue({
|
||||
@@ -53,7 +53,7 @@ test("Can insert and delete Map entries in edit()", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Can get map entry values at different points in time", () => {
|
||||
test("Can get CoMap entry values at different points in time", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
||||
|
||||
const coValue = node.createCoValue({
|
||||
@@ -89,7 +89,7 @@ test("Can get map entry values at different points in time", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Can get all historic values of key", () => {
|
||||
test("Can get all historic values of key in CoMap", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
||||
|
||||
const coValue = node.createCoValue({
|
||||
@@ -141,7 +141,7 @@ test("Can get all historic values of key", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Can get last tx ID for a key", () => {
|
||||
test("Can get last tx ID for a key in CoMap", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
||||
|
||||
const coValue = node.createCoValue({
|
||||
@@ -173,3 +173,112 @@ test("Can get last tx ID for a key", () => {
|
||||
expect(editable.getLastTxID("hello")?.txIndex).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
test("Empty CoList works", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
||||
|
||||
const coValue = node.createCoValue({
|
||||
type: "colist",
|
||||
ruleset: { type: "unsafeAllowAll" },
|
||||
meta: null,
|
||||
...createdNowUnique(),
|
||||
});
|
||||
|
||||
const content = coValue.getCurrentContent();
|
||||
|
||||
if (content.type !== "colist") {
|
||||
throw new Error("Expected list");
|
||||
}
|
||||
|
||||
expect(content.type).toEqual("colist");
|
||||
expect(content.toJSON()).toEqual([]);
|
||||
});
|
||||
|
||||
test("Can append, prepend and delete items to CoList", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
||||
|
||||
const coValue = node.createCoValue({
|
||||
type: "colist",
|
||||
ruleset: { type: "unsafeAllowAll" },
|
||||
meta: null,
|
||||
...createdNowUnique(),
|
||||
});
|
||||
|
||||
const content = coValue.getCurrentContent();
|
||||
|
||||
if (content.type !== "colist") {
|
||||
throw new Error("Expected list");
|
||||
}
|
||||
|
||||
expect(content.type).toEqual("colist");
|
||||
|
||||
content.edit((editable) => {
|
||||
editable.append(0, "hello", "trusting");
|
||||
expect(editable.toJSON()).toEqual(["hello"]);
|
||||
editable.append(0, "world", "trusting");
|
||||
expect(editable.toJSON()).toEqual(["hello", "world"]);
|
||||
editable.prepend(1, "beautiful", "trusting");
|
||||
expect(editable.toJSON()).toEqual(["hello", "beautiful", "world"]);
|
||||
editable.prepend(3, "hooray", "trusting");
|
||||
expect(editable.toJSON()).toEqual([
|
||||
"hello",
|
||||
"beautiful",
|
||||
"world",
|
||||
"hooray",
|
||||
]);
|
||||
editable.delete(2, "trusting");
|
||||
expect(editable.toJSON()).toEqual(["hello", "beautiful", "hooray"]);
|
||||
});
|
||||
});
|
||||
|
||||
test("Push is equivalent to append after last item", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
||||
|
||||
const coValue = node.createCoValue({
|
||||
type: "colist",
|
||||
ruleset: { type: "unsafeAllowAll" },
|
||||
meta: null,
|
||||
...createdNowUnique(),
|
||||
});
|
||||
|
||||
const content = coValue.getCurrentContent();
|
||||
|
||||
if (content.type !== "colist") {
|
||||
throw new Error("Expected list");
|
||||
}
|
||||
|
||||
expect(content.type).toEqual("colist");
|
||||
|
||||
content.edit((editable) => {
|
||||
editable.append(0, "hello", "trusting");
|
||||
expect(editable.toJSON()).toEqual(["hello"]);
|
||||
editable.push("world", "trusting");
|
||||
expect(editable.toJSON()).toEqual(["hello", "world"]);
|
||||
editable.push("hooray", "trusting");
|
||||
expect(editable.toJSON()).toEqual(["hello", "world", "hooray"]);
|
||||
});
|
||||
});
|
||||
|
||||
test("Can push into empty list", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
||||
|
||||
const coValue = node.createCoValue({
|
||||
type: "colist",
|
||||
ruleset: { type: "unsafeAllowAll" },
|
||||
meta: null,
|
||||
...createdNowUnique(),
|
||||
});
|
||||
|
||||
const content = coValue.getCurrentContent();
|
||||
|
||||
if (content.type !== "colist") {
|
||||
throw new Error("Expected list");
|
||||
}
|
||||
|
||||
expect(content.type).toEqual("colist");
|
||||
|
||||
content.edit((editable) => {
|
||||
editable.push("hello", "trusting");
|
||||
expect(editable.toJSON()).toEqual(["hello"]);
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,19 +1,262 @@
|
||||
import { JsonObject, JsonValue } from '../jsonValue.js';
|
||||
import { CoID } from '../contentType.js';
|
||||
import { CoValue } from '../coValue.js';
|
||||
import { JsonObject, JsonValue } from "../jsonValue.js";
|
||||
import { CoID } from "../contentType.js";
|
||||
import { CoValue, accountOrAgentIDfromSessionID } from "../coValue.js";
|
||||
import { SessionID, TransactionID } from "../ids.js";
|
||||
import { AccountID } from "../index.js";
|
||||
import { isAccountID } from "../account.js";
|
||||
|
||||
export class CoList<T extends JsonValue, Meta extends JsonObject | null = null> {
|
||||
type OpID = TransactionID & { changeIdx: number };
|
||||
|
||||
type InsertionOpPayload<T extends JsonValue> =
|
||||
| {
|
||||
op: "pre";
|
||||
value: T;
|
||||
before: OpID | "end";
|
||||
}
|
||||
| {
|
||||
op: "app";
|
||||
value: T;
|
||||
after: OpID | "start";
|
||||
};
|
||||
|
||||
type DeletionOpPayload = {
|
||||
op: "del";
|
||||
insertion: OpID;
|
||||
};
|
||||
|
||||
export type ListOpPayload<T extends JsonValue> =
|
||||
| InsertionOpPayload<T>
|
||||
| DeletionOpPayload;
|
||||
|
||||
type InsertionEntry<T extends JsonValue> = {
|
||||
madeAt: number;
|
||||
predecessors: OpID[];
|
||||
successors: OpID[];
|
||||
} & InsertionOpPayload<T>;
|
||||
|
||||
type DeletionEntry = {
|
||||
madeAt: number;
|
||||
deletionID: OpID;
|
||||
} & DeletionOpPayload;
|
||||
|
||||
export class CoList<
|
||||
T extends JsonValue,
|
||||
Meta extends JsonObject | null = null
|
||||
> {
|
||||
id: CoID<CoList<T, Meta>>;
|
||||
type = "colist" as const;
|
||||
coValue: CoValue;
|
||||
afterStart: OpID[];
|
||||
beforeEnd: OpID[];
|
||||
insertions: {
|
||||
[sessionID: SessionID]: {
|
||||
[txIdx: number]: {
|
||||
[changeIdx: number]: InsertionEntry<T>;
|
||||
};
|
||||
};
|
||||
};
|
||||
deletionsByInsertion: {
|
||||
[deletedSessionID: SessionID]: {
|
||||
[deletedTxIdx: number]: {
|
||||
[deletedChangeIdx: number]: DeletionEntry[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
constructor(coValue: CoValue) {
|
||||
this.id = coValue.id as CoID<CoList<T, Meta>>;
|
||||
this.coValue = coValue;
|
||||
this.afterStart = [];
|
||||
this.beforeEnd = [];
|
||||
this.insertions = {};
|
||||
this.deletionsByInsertion = {};
|
||||
|
||||
this.fillOpsFromCoValue();
|
||||
}
|
||||
|
||||
toJSON(): JsonObject {
|
||||
throw new Error("Method not implemented.");
|
||||
|
||||
get meta(): Meta {
|
||||
return this.coValue.header.meta as Meta;
|
||||
}
|
||||
|
||||
protected fillOpsFromCoValue() {
|
||||
this.insertions = {};
|
||||
this.deletionsByInsertion = {};
|
||||
this.afterStart = [];
|
||||
this.beforeEnd = [];
|
||||
|
||||
for (const {
|
||||
txID,
|
||||
changes,
|
||||
madeAt,
|
||||
} of this.coValue.getValidSortedTransactions()) {
|
||||
for (const [changeIdx, changeUntyped] of changes.entries()) {
|
||||
const change = changeUntyped as ListOpPayload<T>;
|
||||
|
||||
if (change.op === "pre" || change.op === "app") {
|
||||
let sessionEntry = this.insertions[txID.sessionID];
|
||||
if (!sessionEntry) {
|
||||
sessionEntry = {};
|
||||
this.insertions[txID.sessionID] = sessionEntry;
|
||||
}
|
||||
let txEntry = sessionEntry[txID.txIndex];
|
||||
if (!txEntry) {
|
||||
txEntry = {};
|
||||
sessionEntry[txID.txIndex] = txEntry;
|
||||
}
|
||||
txEntry[changeIdx] = {
|
||||
madeAt,
|
||||
predecessors: [],
|
||||
successors: [],
|
||||
...change,
|
||||
};
|
||||
if (change.op === "pre") {
|
||||
if (change.before === "end") {
|
||||
this.beforeEnd.push({
|
||||
...txID,
|
||||
changeIdx,
|
||||
});
|
||||
} else {
|
||||
const beforeEntry =
|
||||
this.insertions[change.before.sessionID]?.[
|
||||
change.before.txIndex
|
||||
]?.[change.before.changeIdx];
|
||||
if (!beforeEntry) {
|
||||
throw new Error(
|
||||
"Not yet implemented: insertion before missing op " +
|
||||
change.before
|
||||
);
|
||||
}
|
||||
beforeEntry.predecessors.splice(0, 0, {
|
||||
...txID,
|
||||
changeIdx,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (change.after === "start") {
|
||||
this.afterStart.push({
|
||||
...txID,
|
||||
changeIdx,
|
||||
});
|
||||
} else {
|
||||
const afterEntry =
|
||||
this.insertions[change.after.sessionID]?.[
|
||||
change.after.txIndex
|
||||
]?.[change.after.changeIdx];
|
||||
if (!afterEntry) {
|
||||
throw new Error(
|
||||
"Not yet implemented: insertion after missing op " +
|
||||
change.after
|
||||
);
|
||||
}
|
||||
afterEntry.successors.push({
|
||||
...txID,
|
||||
changeIdx,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (change.op === "del") {
|
||||
let sessionEntry =
|
||||
this.deletionsByInsertion[change.insertion.sessionID];
|
||||
if (!sessionEntry) {
|
||||
sessionEntry = {};
|
||||
this.deletionsByInsertion[change.insertion.sessionID] =
|
||||
sessionEntry;
|
||||
}
|
||||
let txEntry = sessionEntry[change.insertion.txIndex];
|
||||
if (!txEntry) {
|
||||
txEntry = {};
|
||||
sessionEntry[change.insertion.txIndex] = txEntry;
|
||||
}
|
||||
let changeEntry = txEntry[change.insertion.changeIdx];
|
||||
if (!changeEntry) {
|
||||
changeEntry = [];
|
||||
txEntry[change.insertion.changeIdx] = changeEntry;
|
||||
}
|
||||
changeEntry.push({
|
||||
madeAt,
|
||||
deletionID: {
|
||||
...txID,
|
||||
changeIdx,
|
||||
},
|
||||
...change,
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
"Unknown list operation " +
|
||||
(change as { op: unknown }).op
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entries(): { value: T; madeAt: number; opID: OpID }[] {
|
||||
const arr: { value: T; madeAt: number; opID: OpID }[] = [];
|
||||
for (const opID of this.afterStart) {
|
||||
this.fillArrayFromOpID(opID, arr);
|
||||
}
|
||||
for (const opID of this.beforeEnd) {
|
||||
this.fillArrayFromOpID(opID, arr);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
private fillArrayFromOpID(
|
||||
opID: OpID,
|
||||
arr: { value: T; madeAt: number; opID: OpID }[]
|
||||
) {
|
||||
const entry =
|
||||
this.insertions[opID.sessionID]?.[opID.txIndex]?.[opID.changeIdx];
|
||||
if (!entry) {
|
||||
throw new Error("Missing op " + opID);
|
||||
}
|
||||
for (const predecessor of entry.predecessors) {
|
||||
this.fillArrayFromOpID(predecessor, arr);
|
||||
}
|
||||
const deleted =
|
||||
(this.deletionsByInsertion[opID.sessionID]?.[opID.txIndex]?.[
|
||||
opID.changeIdx
|
||||
]?.length || 0) > 0;
|
||||
if (!deleted) {
|
||||
arr.push({
|
||||
value: entry.value,
|
||||
madeAt: entry.madeAt,
|
||||
opID,
|
||||
});
|
||||
}
|
||||
for (const successor of entry.successors) {
|
||||
this.fillArrayFromOpID(successor, arr);
|
||||
}
|
||||
}
|
||||
|
||||
getLastEditor(idx: number): AccountID | undefined {
|
||||
const entry = this.entries()[idx];
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
const accountID = accountOrAgentIDfromSessionID(entry.opID.sessionID);
|
||||
if (isAccountID(accountID)) {
|
||||
return accountID;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): T[] {
|
||||
return this.asArray();
|
||||
}
|
||||
|
||||
asArray(): T[] {
|
||||
return this.entries().map((entry) => entry.value);
|
||||
}
|
||||
|
||||
edit(
|
||||
changer: (editable: WriteableCoList<T, Meta>) => void
|
||||
): CoList<T, Meta> {
|
||||
const editable = new WriteableCoList<T, Meta>(this.coValue);
|
||||
changer(editable);
|
||||
return new CoList(this.coValue);
|
||||
}
|
||||
|
||||
subscribe(listener: (coMap: CoList<T, Meta>) => void): () => void {
|
||||
@@ -22,3 +265,106 @@ export class CoList<T extends JsonValue, Meta extends JsonObject | null = null>
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class WriteableCoList<
|
||||
T extends JsonValue,
|
||||
Meta extends JsonObject | null = null
|
||||
> extends CoList<T, Meta> {
|
||||
append(
|
||||
after: number,
|
||||
value: T,
|
||||
privacy: "private" | "trusting" = "private"
|
||||
): void {
|
||||
const entries = this.entries();
|
||||
let opIDBefore;
|
||||
if (entries.length > 0) {
|
||||
const entryBefore = entries[after];
|
||||
if (!entryBefore) {
|
||||
throw new Error("Invalid index " + after);
|
||||
}
|
||||
opIDBefore = entryBefore.opID;
|
||||
} else {
|
||||
if (after !== 0) {
|
||||
throw new Error("Invalid index " + after);
|
||||
}
|
||||
opIDBefore = "start";
|
||||
}
|
||||
this.coValue.makeTransaction(
|
||||
[
|
||||
{
|
||||
op: "app",
|
||||
value,
|
||||
after: opIDBefore,
|
||||
},
|
||||
],
|
||||
privacy
|
||||
);
|
||||
|
||||
this.fillOpsFromCoValue();
|
||||
}
|
||||
|
||||
push(value: T, privacy: "private" | "trusting" = "private"): void {
|
||||
// TODO: optimize
|
||||
const entries = this.entries();
|
||||
this.append(entries.length > 0 ? entries.length - 1 : 0, value, privacy);
|
||||
}
|
||||
|
||||
prepend(
|
||||
before: number,
|
||||
value: T,
|
||||
privacy: "private" | "trusting" = "private"
|
||||
): void {
|
||||
const entries = this.entries();
|
||||
let opIDAfter;
|
||||
if (entries.length > 0) {
|
||||
const entryAfter = entries[before];
|
||||
if (entryAfter) {
|
||||
opIDAfter = entryAfter.opID;
|
||||
} else {
|
||||
if (before !== entries.length) {
|
||||
throw new Error("Invalid index " + before);
|
||||
}
|
||||
opIDAfter = "end";
|
||||
}
|
||||
} else {
|
||||
if (before !== 0) {
|
||||
throw new Error("Invalid index " + before);
|
||||
}
|
||||
opIDAfter = "end";
|
||||
}
|
||||
this.coValue.makeTransaction(
|
||||
[
|
||||
{
|
||||
op: "pre",
|
||||
value,
|
||||
before: opIDAfter,
|
||||
},
|
||||
],
|
||||
privacy
|
||||
);
|
||||
|
||||
this.fillOpsFromCoValue();
|
||||
}
|
||||
|
||||
delete(
|
||||
at: number,
|
||||
privacy: "private" | "trusting" = "private"
|
||||
): void {
|
||||
const entries = this.entries();
|
||||
const entry = entries[at];
|
||||
if (!entry) {
|
||||
throw new Error("Invalid index " + at);
|
||||
}
|
||||
this.coValue.makeTransaction(
|
||||
[
|
||||
{
|
||||
op: "del",
|
||||
insertion: entry.opID,
|
||||
},
|
||||
],
|
||||
privacy
|
||||
);
|
||||
|
||||
this.fillOpsFromCoValue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,10 @@ export class CoMap<
|
||||
this.fillOpsFromCoValue();
|
||||
}
|
||||
|
||||
get meta(): Meta {
|
||||
return this.coValue.header.meta as Meta;
|
||||
}
|
||||
|
||||
protected fillOpsFromCoValue() {
|
||||
this.ops = {};
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "./account.js";
|
||||
import { Role } from "./permissions.js";
|
||||
import { base58 } from "@scure/base";
|
||||
import { CoList } from "./contentTypes/coList.js";
|
||||
|
||||
export type GroupContent = {
|
||||
profile: CoID<Profile> | null;
|
||||
@@ -186,10 +187,9 @@ export class Group {
|
||||
this.rotateReadKey();
|
||||
}
|
||||
|
||||
createMap<
|
||||
M extends { [key: string]: JsonValue },
|
||||
Meta extends JsonObject | null = null
|
||||
>(meta?: Meta): CoMap<M, Meta> {
|
||||
createMap<M extends CoMap<{ [key: string]: JsonValue }, JsonObject | null>>(
|
||||
meta?: M["meta"]
|
||||
): M {
|
||||
return this.node
|
||||
.createCoValue({
|
||||
type: "comap",
|
||||
@@ -200,7 +200,23 @@ export class Group {
|
||||
meta: meta || null,
|
||||
...createdNowUnique(),
|
||||
})
|
||||
.getCurrentContent() as CoMap<M, Meta>;
|
||||
.getCurrentContent() as M;
|
||||
}
|
||||
|
||||
createList<L extends CoList<JsonValue, JsonObject | null>>(
|
||||
meta?: L["meta"]
|
||||
): L {
|
||||
return this.node
|
||||
.createCoValue({
|
||||
type: "colist",
|
||||
ruleset: {
|
||||
type: "ownedByGroup",
|
||||
group: this.groupMap.id,
|
||||
},
|
||||
meta: meta || null,
|
||||
...createdNowUnique(),
|
||||
})
|
||||
.getCurrentContent() as L;
|
||||
}
|
||||
|
||||
testWithDifferentAccount(
|
||||
@@ -230,4 +246,4 @@ export function secretSeedFromInviteSecret(inviteSecret: InviteSecret) {
|
||||
}
|
||||
|
||||
return base58.decode(inviteSecret.slice("inviteSecret_z".length));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
AccountID,
|
||||
AccountContent,
|
||||
ProfileContent,
|
||||
ProfileMeta,
|
||||
Profile,
|
||||
} from "./account.js";
|
||||
import type { InviteSecret } from "./group.js";
|
||||
@@ -70,6 +71,7 @@ export type {
|
||||
AccountContent,
|
||||
Profile,
|
||||
ProfileContent,
|
||||
ProfileMeta,
|
||||
InviteSecret
|
||||
};
|
||||
|
||||
|
||||
@@ -31,8 +31,7 @@ import {
|
||||
AccountID,
|
||||
Profile,
|
||||
AccountContent,
|
||||
ProfileContent,
|
||||
ProfileMeta,
|
||||
AccountMap,
|
||||
} from "./account.js";
|
||||
import { CoMap } from "./index.js";
|
||||
|
||||
@@ -139,7 +138,7 @@ export class LocalNode {
|
||||
}
|
||||
|
||||
async loadProfile(id: AccountID): Promise<Profile> {
|
||||
const account = await this.load<CoMap<AccountContent>>(id);
|
||||
const account = await this.load<AccountMap>(id);
|
||||
const profileID = account.get("profile");
|
||||
|
||||
if (!profileID) {
|
||||
@@ -307,7 +306,7 @@ export class LocalNode {
|
||||
account.node
|
||||
);
|
||||
|
||||
const profile = accountAsGroup.createMap<ProfileContent, ProfileMeta>({
|
||||
const profile = accountAsGroup.createMap<Profile>({
|
||||
type: "profile",
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { ReadableStream, TransformStream, WritableStream } from "isomorphic-streams";
|
||||
import {
|
||||
ReadableStream,
|
||||
TransformStream,
|
||||
WritableStream,
|
||||
} from "isomorphic-streams";
|
||||
import { Peer, PeerID, SyncMessage } from "./sync.js";
|
||||
|
||||
|
||||
export function connectedPeers(
|
||||
peer1id: PeerID,
|
||||
peer2id: PeerID,
|
||||
{
|
||||
trace = false, peer1role = "peer", peer2role = "peer",
|
||||
trace = false,
|
||||
peer1role = "peer",
|
||||
peer2role = "peer",
|
||||
}: {
|
||||
trace?: boolean;
|
||||
peer1role?: Peer["role"];
|
||||
@@ -24,9 +29,13 @@ export function connectedPeers(
|
||||
new TransformStream({
|
||||
transform(
|
||||
chunk: SyncMessage,
|
||||
controller: { enqueue: (msg: SyncMessage) => void; }
|
||||
controller: { enqueue: (msg: SyncMessage) => void }
|
||||
) {
|
||||
trace && console.debug(`${peer2id} -> ${peer1id}`, JSON.stringify(chunk, null, 2));
|
||||
trace &&
|
||||
console.debug(
|
||||
`${peer2id} -> ${peer1id}`,
|
||||
JSON.stringify(chunk, null, 2)
|
||||
);
|
||||
controller.enqueue(chunk);
|
||||
},
|
||||
})
|
||||
@@ -38,9 +47,13 @@ export function connectedPeers(
|
||||
new TransformStream({
|
||||
transform(
|
||||
chunk: SyncMessage,
|
||||
controller: { enqueue: (msg: SyncMessage) => void; }
|
||||
controller: { enqueue: (msg: SyncMessage) => void }
|
||||
) {
|
||||
trace && console.debug(`${peer1id} -> ${peer2id}`, JSON.stringify(chunk, null, 2));
|
||||
trace &&
|
||||
console.debug(
|
||||
`${peer1id} -> ${peer2id}`,
|
||||
JSON.stringify(chunk, null, 2)
|
||||
);
|
||||
controller.enqueue(chunk);
|
||||
},
|
||||
})
|
||||
@@ -65,39 +78,22 @@ export function connectedPeers(
|
||||
}
|
||||
|
||||
export function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
|
||||
const queue: T[] = [];
|
||||
let resolveNextItemReady: () => void = () => { };
|
||||
let nextItemReady: Promise<void> = new Promise((resolve) => {
|
||||
resolveNextItemReady = resolve;
|
||||
});
|
||||
|
||||
let writerClosed = false;
|
||||
let readerClosed = false;
|
||||
|
||||
let resolveEnqueue: (enqueue: (item: T) => void) => void;
|
||||
const enqueuePromise = new Promise<(item: T) => void>((resolve) => {
|
||||
resolveEnqueue = resolve;
|
||||
});
|
||||
|
||||
let resolveClose: (close: () => void) => void;
|
||||
const closePromise = new Promise<() => void>((resolve) => {
|
||||
resolveClose = resolve;
|
||||
});
|
||||
|
||||
const readable = new ReadableStream<T>({
|
||||
async pull(controller) {
|
||||
let retriesLeft = 3;
|
||||
while (retriesLeft > 0) {
|
||||
if (writerClosed) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
retriesLeft--;
|
||||
if (queue.length > 0) {
|
||||
controller.enqueue(queue.shift()!);
|
||||
if (queue.length === 0) {
|
||||
nextItemReady = new Promise((resolve) => {
|
||||
resolveNextItemReady = resolve;
|
||||
});
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
await nextItemReady;
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
"Should only use one retry to get next item in queue."
|
||||
);
|
||||
async start(controller) {
|
||||
resolveEnqueue(controller.enqueue.bind(controller));
|
||||
resolveClose(controller.close.bind(controller));
|
||||
},
|
||||
|
||||
cancel(_reason) {
|
||||
@@ -107,22 +103,21 @@ export function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
|
||||
});
|
||||
|
||||
const writable = new WritableStream<T>({
|
||||
write(chunk) {
|
||||
async write(chunk) {
|
||||
const enqueue = await enqueuePromise;
|
||||
if (readerClosed) {
|
||||
console.log("Reader closed, not writing chunk", chunk);
|
||||
throw new Error("Reader closed, not writing chunk");
|
||||
}
|
||||
queue.push(chunk);
|
||||
if (queue.length === 1) {
|
||||
// make sure that await write resolves before corresponding read
|
||||
setTimeout(() => resolveNextItemReady());
|
||||
throw new Error("Reader closed");
|
||||
} else {
|
||||
// make sure write resolves before corresponding read
|
||||
setTimeout(() => {
|
||||
enqueue(chunk);
|
||||
})
|
||||
}
|
||||
},
|
||||
abort(_reason) {
|
||||
console.log("Manually closing writer");
|
||||
writerClosed = true;
|
||||
resolveNextItemReady();
|
||||
return Promise.resolve();
|
||||
async abort(reason) {
|
||||
console.debug("Manually closing writer", reason);
|
||||
const close = await closePromise;
|
||||
close();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -268,6 +268,7 @@ export class SyncManager {
|
||||
);
|
||||
}
|
||||
}
|
||||
console.log("DONE!!!");
|
||||
} catch (e) {
|
||||
console.error(`Error reading from peer ${peer.id}`, e);
|
||||
}
|
||||
@@ -280,13 +281,32 @@ export class SyncManager {
|
||||
}
|
||||
|
||||
trySendToPeer(peer: PeerState, msg: SyncMessage) {
|
||||
return peer.outgoing.write(msg).catch((e) => {
|
||||
console.error(
|
||||
new Error(`Error writing to peer ${peer.id}, disconnecting`, {
|
||||
cause: e,
|
||||
return new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.error(
|
||||
new Error(
|
||||
`Writing to peer ${peer.id} took >1s - this should never happen as write should resolve quickly or error`
|
||||
)
|
||||
);
|
||||
resolve();
|
||||
}, 1000);
|
||||
peer.outgoing
|
||||
.write(msg)
|
||||
.then(() => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
})
|
||||
);
|
||||
delete this.peers[peer.id];
|
||||
.catch((e) => {
|
||||
console.error(
|
||||
new Error(
|
||||
`Error writing to peer ${peer.id}, disconnecting`,
|
||||
{
|
||||
cause: e,
|
||||
}
|
||||
)
|
||||
);
|
||||
delete this.peers[peer.id];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -295,7 +315,19 @@ export class SyncManager {
|
||||
|
||||
if (!entry || entry.state === "loading") {
|
||||
if (!entry) {
|
||||
this.local.coValues[msg.id] = newLoadingState();
|
||||
await new Promise<void>((resolve) => {
|
||||
this.local
|
||||
.loadCoValue(msg.id)
|
||||
.then(() => resolve())
|
||||
.catch((e) => {
|
||||
console.error(
|
||||
"Error loading coValue in handleLoad",
|
||||
e
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
peer.optimisticKnownStates[msg.id] = knownStateIn(msg);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "jazz-browser-auth-local",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.6",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jazz-browser": "^0.1.3",
|
||||
"jazz-browser": "^0.1.6",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jazz-browser",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.6",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.1.3",
|
||||
"jazz-storage-indexeddb": "^0.1.3",
|
||||
"cojson": "^0.1.6",
|
||||
"jazz-storage-indexeddb": "^0.1.6",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -182,10 +182,12 @@ function websocketReadableStream<T>(ws: WebSocket) {
|
||||
}
|
||||
controller.enqueue(msg);
|
||||
};
|
||||
ws.addEventListener("close", () => controller.close());
|
||||
ws.addEventListener("error", () =>
|
||||
controller.error(new Error("The WebSocket errored!"))
|
||||
);
|
||||
const closeListener = () => controller.close();
|
||||
ws.addEventListener("close", closeListener);
|
||||
ws.addEventListener("error", () => {
|
||||
controller.error(new Error("The WebSocket errored!"));
|
||||
ws.removeEventListener("close", closeListener);
|
||||
});
|
||||
},
|
||||
|
||||
cancel() {
|
||||
@@ -209,23 +211,37 @@ function createWebSocketPeer(syncAddress: string): Peer {
|
||||
}
|
||||
|
||||
function websocketWritableStream<T>(ws: WebSocket) {
|
||||
const initialQueue = [] as T[];
|
||||
let isOpen = false;
|
||||
|
||||
return new WritableStream<T>({
|
||||
start(controller) {
|
||||
ws.addEventListener("error", () => {
|
||||
controller.error(new Error("The WebSocket errored!"));
|
||||
ws.addEventListener("error", (event) => {
|
||||
controller.error(
|
||||
new Error("The WebSocket errored!" + JSON.stringify(event))
|
||||
);
|
||||
});
|
||||
ws.addEventListener("close", () => {
|
||||
controller.error(
|
||||
new Error("The server closed the connection unexpectedly!")
|
||||
);
|
||||
});
|
||||
return new Promise((resolve) => (ws.addEventListener("open", resolve)));
|
||||
ws.addEventListener("open", () => {
|
||||
for (const item of initialQueue) {
|
||||
ws.send(JSON.stringify(item));
|
||||
}
|
||||
isOpen = true;
|
||||
});
|
||||
},
|
||||
|
||||
write(chunk) {
|
||||
ws.send(JSON.stringify(chunk));
|
||||
// Return immediately, since the web socket gives us no easy way to tell
|
||||
// when the write completes.
|
||||
async write(chunk) {
|
||||
if (isOpen) {
|
||||
ws.send(JSON.stringify(chunk));
|
||||
// Return immediately, since the web socket gives us no easy way to tell
|
||||
// when the write completes.
|
||||
} else {
|
||||
initialQueue.push(chunk);
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jazz-react-auth-local",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.7",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jazz-browser-auth-local": "^0.1.3",
|
||||
"jazz-react": "^0.1.4",
|
||||
"jazz-browser-auth-local": "^0.1.6",
|
||||
"jazz-react": "^0.1.7",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jazz-react",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.7",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.1.3",
|
||||
"jazz-browser": "^0.1.3",
|
||||
"cojson": "^0.1.6",
|
||||
"jazz-browser": "^0.1.6",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,9 +3,10 @@ import {
|
||||
ContentType,
|
||||
CoID,
|
||||
ProfileContent,
|
||||
ProfileMeta,
|
||||
CoMap,
|
||||
AccountID,
|
||||
Profile,
|
||||
JsonValue,
|
||||
} from "cojson";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { AuthProvider, createBrowserNode } from "jazz-browser";
|
||||
@@ -123,10 +124,10 @@ export function useTelepathicState<T extends ContentType>(id?: CoID<T>) {
|
||||
return state;
|
||||
}
|
||||
|
||||
export function useProfile<P extends ProfileContent = ProfileContent>(
|
||||
accountID?: AccountID
|
||||
): (Profile & CoMap<P>) | undefined {
|
||||
const [profileID, setProfileID] = useState<CoID<Profile & CoMap<P>>>();
|
||||
export function useProfile<
|
||||
P extends { [key: string]: JsonValue } & ProfileContent = ProfileContent
|
||||
>(accountID?: AccountID): CoMap<P, ProfileMeta> | undefined {
|
||||
const [profileID, setProfileID] = useState<CoID<CoMap<P, ProfileMeta>>>();
|
||||
|
||||
const { localNode } = useJazz();
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "jazz-storage-indexeddb",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.6",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.1.3",
|
||||
"cojson": "^0.1.6",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
235
yarn.lock
235
yarn.lock
@@ -1513,6 +1513,13 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.20.7"
|
||||
|
||||
"@types/better-sqlite3@^7.6.4":
|
||||
version "7.6.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/better-sqlite3/-/better-sqlite3-7.6.4.tgz#102462611e67aadf950d3ccca10292de91e6f35b"
|
||||
integrity sha512-dzrRZCYPXIXfSR1/surNbJ/grU3scTaygS0OMzjlGf71i9sc2fGyHPXXiXmEvNIoE0cGwsanEFMVJxPXmco9Eg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/chai-subset@^1.3.3":
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94"
|
||||
@@ -1604,6 +1611,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
||||
|
||||
"@types/qrcode@^1.5.1":
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.1.tgz#027c2dbfbc8505e1fe2f4033daba920dbd182b44"
|
||||
integrity sha512-HpSN675K0PmxIDRpjMI3Mc2GiKo3dNu+X/F5SoItiaDS1lVfgC6Wac1c5lQDfKWbTJUSHWiHKzpJpBZG7k9gaA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/react-dom@^18.2.7":
|
||||
version "18.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.7.tgz#67222a08c0a6ae0a0da33c3532348277c70abb63"
|
||||
@@ -2243,6 +2257,14 @@ before-after-hook@^2.2.0:
|
||||
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c"
|
||||
integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==
|
||||
|
||||
better-sqlite3@^8.5.2:
|
||||
version "8.5.2"
|
||||
resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-8.5.2.tgz#a1c13e4361125255e39302e8b569a6568c3291e3"
|
||||
integrity sha512-w/EZ/jwuZF+/47mAVC2+rhR2X/gwkZ+fd1pbX7Y90D5NRaRzDQcxrHY10t6ijGiYIonCVsBSF5v1cay07bP5sg==
|
||||
dependencies:
|
||||
bindings "^1.5.0"
|
||||
prebuild-install "^7.1.0"
|
||||
|
||||
big-integer@^1.6.17:
|
||||
version "1.6.51"
|
||||
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686"
|
||||
@@ -2261,6 +2283,13 @@ binary@~0.3.0:
|
||||
buffers "~0.1.1"
|
||||
chainsaw "~0.1.0"
|
||||
|
||||
bindings@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
|
||||
integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
|
||||
dependencies:
|
||||
file-uri-to-path "1.0.0"
|
||||
|
||||
bl@^4.0.3, bl@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
|
||||
@@ -2433,7 +2462,7 @@ camelcase-keys@^6.2.2:
|
||||
map-obj "^4.0.0"
|
||||
quick-lru "^4.0.1"
|
||||
|
||||
camelcase@^5.3.1:
|
||||
camelcase@^5.0.0, camelcase@^5.3.1:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
||||
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
||||
@@ -2528,6 +2557,11 @@ chokidar@^3.5.3:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
chownr@^1.1.1:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
|
||||
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
|
||||
|
||||
chownr@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
|
||||
@@ -2594,6 +2628,15 @@ cli-width@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
|
||||
integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
|
||||
|
||||
cliui@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
|
||||
integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
|
||||
cliui@^7.0.2:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
||||
@@ -2927,7 +2970,7 @@ decamelize-keys@^1.1.0:
|
||||
decamelize "^1.1.0"
|
||||
map-obj "^1.0.0"
|
||||
|
||||
decamelize@^1.1.0:
|
||||
decamelize@^1.1.0, decamelize@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
|
||||
@@ -2961,6 +3004,11 @@ deep-eql@^4.1.2:
|
||||
dependencies:
|
||||
type-detect "^4.0.0"
|
||||
|
||||
deep-extend@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
|
||||
|
||||
deep-is@^0.1.3:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||
@@ -3027,6 +3075,11 @@ detect-indent@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
|
||||
integrity sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==
|
||||
|
||||
detect-libc@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d"
|
||||
integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==
|
||||
|
||||
detect-newline@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
|
||||
@@ -3052,6 +3105,11 @@ diff-sequences@^29.4.3:
|
||||
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2"
|
||||
integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==
|
||||
|
||||
dijkstrajs@^1.0.1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23"
|
||||
integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==
|
||||
|
||||
dir-glob@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
||||
@@ -3148,6 +3206,11 @@ emoji-regex@^9.2.2:
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
|
||||
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
|
||||
|
||||
encode-utf8@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda"
|
||||
integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
|
||||
|
||||
encoding@^0.1.13:
|
||||
version "0.1.13"
|
||||
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
|
||||
@@ -3543,6 +3606,11 @@ exit@^0.1.2:
|
||||
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
|
||||
integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==
|
||||
|
||||
expand-template@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
|
||||
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
|
||||
|
||||
expect@^29.0.0, expect@^29.6.2:
|
||||
version "29.6.2"
|
||||
resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.2.tgz#7b08e83eba18ddc4a2cf62b5f2d1918f5cd84521"
|
||||
@@ -3674,6 +3742,11 @@ file-entry-cache@^6.0.1:
|
||||
dependencies:
|
||||
flat-cache "^3.0.4"
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
|
||||
|
||||
filelist@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5"
|
||||
@@ -3871,7 +3944,7 @@ gensync@^1.0.0-beta.2:
|
||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
||||
|
||||
get-caller-file@^2.0.5:
|
||||
get-caller-file@^2.0.1, get-caller-file@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
@@ -3980,6 +4053,11 @@ gitconfiglocal@^1.0.0:
|
||||
dependencies:
|
||||
ini "^1.3.2"
|
||||
|
||||
github-from-package@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
|
||||
integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==
|
||||
|
||||
glob-parent@5.1.2, glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||
@@ -4359,7 +4437,7 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3:
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
ini@^1.3.2, ini@^1.3.8:
|
||||
ini@^1.3.2, ini@^1.3.8, ini@~1.3.0:
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
|
||||
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
|
||||
@@ -5614,7 +5692,7 @@ minimist-options@4.1.0:
|
||||
is-plain-obj "^1.1.0"
|
||||
kind-of "^6.0.3"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
|
||||
minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
@@ -5701,7 +5779,7 @@ mitt@3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd"
|
||||
integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==
|
||||
|
||||
mkdirp-classic@^0.5.2:
|
||||
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
|
||||
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
|
||||
@@ -5798,6 +5876,11 @@ nanoid@^3.3.6:
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
||||
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
|
||||
|
||||
napi-build-utils@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
|
||||
integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
@@ -5826,6 +5909,13 @@ nice-napi@^1.0.2:
|
||||
node-addon-api "^3.0.0"
|
||||
node-gyp-build "^4.2.2"
|
||||
|
||||
node-abi@^3.3.0:
|
||||
version "3.47.0"
|
||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.47.0.tgz#6cbfa2916805ae25c2b7156ca640131632eb05e8"
|
||||
integrity sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==
|
||||
dependencies:
|
||||
semver "^7.3.5"
|
||||
|
||||
node-addon-api@^3.0.0, node-addon-api@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
|
||||
@@ -6521,6 +6611,11 @@ pkg-types@^1.0.3:
|
||||
mlly "^1.2.0"
|
||||
pathe "^1.1.0"
|
||||
|
||||
pngjs@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
|
||||
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
|
||||
|
||||
postcss-import@^15.1.0:
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70"
|
||||
@@ -6574,6 +6669,24 @@ postcss@^8.4.23, postcss@^8.4.27:
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
prebuild-install@^7.1.0:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45"
|
||||
integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==
|
||||
dependencies:
|
||||
detect-libc "^2.0.0"
|
||||
expand-template "^2.0.3"
|
||||
github-from-package "0.0.0"
|
||||
minimist "^1.2.3"
|
||||
mkdirp-classic "^0.5.3"
|
||||
napi-build-utils "^1.0.1"
|
||||
node-abi "^3.3.0"
|
||||
pump "^3.0.0"
|
||||
rc "^1.2.7"
|
||||
simple-get "^4.0.0"
|
||||
tar-fs "^2.0.0"
|
||||
tunnel-agent "^0.6.0"
|
||||
|
||||
prelude-ls@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
@@ -6685,6 +6798,16 @@ pure-rand@^6.0.0:
|
||||
resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306"
|
||||
integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==
|
||||
|
||||
qrcode@^1.5.3:
|
||||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.3.tgz#03afa80912c0dccf12bc93f615a535aad1066170"
|
||||
integrity sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==
|
||||
dependencies:
|
||||
dijkstrajs "^1.0.1"
|
||||
encode-utf8 "^1.0.3"
|
||||
pngjs "^5.0.0"
|
||||
yargs "^15.3.1"
|
||||
|
||||
query-selector-shadow-dom@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz#1c7b0058eff4881ac44f45d8f84ede32e9a2f349"
|
||||
@@ -6710,6 +6833,16 @@ quick-lru@^5.1.1:
|
||||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
|
||||
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
|
||||
|
||||
rc@^1.2.7:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
|
||||
dependencies:
|
||||
deep-extend "^0.6.0"
|
||||
ini "~1.3.0"
|
||||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
react-dom@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||
@@ -6876,6 +7009,11 @@ require-directory@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
|
||||
|
||||
require-main-filename@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
|
||||
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
|
||||
|
||||
resolve-alpn@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9"
|
||||
@@ -7001,16 +7139,16 @@ safaridriver@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/safaridriver/-/safaridriver-0.1.0.tgz#8ff901e847b003c6a52b534028f57cddc82d6b14"
|
||||
integrity sha512-azzzIP3gR1TB9bVPv7QO4Zjw0rR1BWEU/s2aFdUMN48gxDjxEB13grAEuXDmkKPgE74cObymDxmAmZnL3clj4w==
|
||||
|
||||
safe-buffer@^5.0.1, safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
|
||||
safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
@@ -7109,6 +7247,20 @@ sigstore@^1.3.0, sigstore@^1.4.0:
|
||||
"@sigstore/tuf" "^1.0.3"
|
||||
make-fetch-happen "^11.0.1"
|
||||
|
||||
simple-concat@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
|
||||
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
|
||||
|
||||
simple-get@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
|
||||
integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==
|
||||
dependencies:
|
||||
decompress-response "^6.0.0"
|
||||
once "^1.3.1"
|
||||
simple-concat "^1.0.0"
|
||||
|
||||
sirv@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.3.tgz#ca5868b87205a74bef62a469ed0296abceccd446"
|
||||
@@ -7349,6 +7501,11 @@ strip-json-comments@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
strip-json-comments@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
|
||||
|
||||
strip-literal@^1.0.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-1.3.0.tgz#db3942c2ec1699e6836ad230090b84bb458e3a07"
|
||||
@@ -7451,7 +7608,17 @@ tar-fs@3.0.4, tar-fs@^3.0.4:
|
||||
pump "^3.0.0"
|
||||
tar-stream "^3.1.5"
|
||||
|
||||
tar-stream@^2.2.0, tar-stream@~2.2.0:
|
||||
tar-fs@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
|
||||
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
|
||||
dependencies:
|
||||
chownr "^1.1.1"
|
||||
mkdirp-classic "^0.5.2"
|
||||
pump "^3.0.0"
|
||||
tar-stream "^2.1.4"
|
||||
|
||||
tar-stream@^2.1.4, tar-stream@^2.2.0, tar-stream@~2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
|
||||
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
|
||||
@@ -7659,6 +7826,13 @@ tuf-js@^1.1.7:
|
||||
debug "^4.3.4"
|
||||
make-fetch-happen "^11.1.1"
|
||||
|
||||
tunnel-agent@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
|
||||
integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||
@@ -8001,6 +8175,11 @@ whatwg-url@^5.0.0:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
which-module@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
|
||||
integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
|
||||
|
||||
which@^2.0.1, which@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
||||
@@ -8045,7 +8224,7 @@ wordwrap@^1.0.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^6.0.1:
|
||||
wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
|
||||
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
|
||||
@@ -8124,6 +8303,11 @@ xtend@~4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
y18n@^4.0.0:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
|
||||
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
@@ -8154,6 +8338,14 @@ yargs-parser@21.1.1, yargs-parser@^21.0.1, yargs-parser@^21.1.1:
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||
|
||||
yargs-parser@^18.1.2:
|
||||
version "18.1.3"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
|
||||
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
|
||||
dependencies:
|
||||
camelcase "^5.0.0"
|
||||
decamelize "^1.2.0"
|
||||
|
||||
yargs-parser@^20.2.2, yargs-parser@^20.2.3:
|
||||
version "20.2.9"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
|
||||
@@ -8185,6 +8377,23 @@ yargs@17.7.1:
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^21.1.1"
|
||||
|
||||
yargs@^15.3.1:
|
||||
version "15.4.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
|
||||
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
||||
dependencies:
|
||||
cliui "^6.0.0"
|
||||
decamelize "^1.2.0"
|
||||
find-up "^4.1.0"
|
||||
get-caller-file "^2.0.1"
|
||||
require-directory "^2.1.1"
|
||||
require-main-filename "^2.0.0"
|
||||
set-blocking "^2.0.0"
|
||||
string-width "^4.2.0"
|
||||
which-module "^2.0.0"
|
||||
y18n "^4.0.0"
|
||||
yargs-parser "^18.1.2"
|
||||
|
||||
yargs@^17.3.1, yargs@^17.6.2:
|
||||
version "17.7.2"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
|
||||
|
||||
Reference in New Issue
Block a user