Compare commits

..

18 Commits

Author SHA1 Message Date
Anselm
17d17833b2 Publish
- jazz-example-pets@0.0.14
 - jazz-example-todo@0.0.39
 - cojson@0.3.5
 - cojson-simple-sync@0.3.7
 - cojson-storage-indexeddb@0.3.5
 - cojson-storage-sqlite@0.3.7
 - jazz-browser@0.3.5
 - jazz-browser-auth-local@0.3.5
 - jazz-browser-media-images@0.3.5
 - jazz-react@0.3.5
 - jazz-react-auth-local@0.3.5
 - jazz-react-media-images@0.3.5
2023-09-22 15:18:21 +01:00
Anselm
8e22bd9c1e Lint fix 2023-09-22 15:17:44 +01:00
Anselm
98213743f3 deploy bump 2023-09-22 15:15:09 +01:00
Anselm
bb855ed83d Publish
- jazz-example-pets@0.0.13
 - jazz-example-todo@0.0.38
 - cojson@0.3.4
 - cojson-simple-sync@0.3.6
 - cojson-storage-indexeddb@0.3.4
 - cojson-storage-sqlite@0.3.6
 - jazz-browser@0.3.4
 - jazz-browser-auth-local@0.3.4
 - jazz-browser-media-images@0.3.4
 - jazz-react@0.3.4
 - jazz-react-auth-local@0.3.4
 - jazz-react-media-images@0.3.4
2023-09-22 14:33:25 +01:00
Anselm
a8ef49e228 Small lint fixes 2023-09-22 14:32:41 +01:00
Anselm
e0ad32dbd2 Implement exponential falloff, fixes #69 2023-09-22 14:30:55 +01:00
Anselm
62bf769cad Publish
- cojson-simple-sync@0.3.5
 - cojson-storage-sqlite@0.3.5
2023-09-22 10:36:17 +01:00
Anselm
7488ff25b2 Missed one bit of JSON parsing to make more robust 2023-09-22 10:36:02 +01:00
Anselm
b69c9da983 Publish
- cojson-simple-sync@0.3.4
 - cojson-storage-sqlite@0.3.4
2023-09-22 10:25:25 +01:00
Anselm
d30fdef8aa More JSON.parse resiliency in cojson-storage-sqlite 2023-09-22 10:25:08 +01:00
Anselm
9c5a6b9833 Publish
- jazz-example-pets@0.0.12
 - jazz-example-todo@0.0.37
 - cojson@0.3.3
 - cojson-simple-sync@0.3.3
 - cojson-storage-indexeddb@0.3.3
 - cojson-storage-sqlite@0.3.3
 - jazz-browser@0.3.3
 - jazz-browser-auth-local@0.3.3
 - jazz-browser-media-images@0.3.3
 - jazz-react@0.3.3
 - jazz-react-auth-local@0.3.3
 - jazz-react-media-images@0.3.3
2023-09-22 10:09:04 +01:00
Anselm
d300d265c4 manually update cojson 2023-09-22 10:07:55 +01:00
Anselm
1d72ce587f Update version 2023-09-22 09:53:25 +01:00
Anselm
3fdb41dcb9 More resilience against invalid JSON 2023-09-22 09:51:07 +01:00
Anselm
f20de2f04a v0.3.1 2023-09-22 09:36:32 +01:00
Anselm
31b31f111b Shorter logs on failed transactions 2023-09-22 09:34:54 +01:00
Anselm Eickhoff
2ae9fb9778 Fix example comment 2023-09-21 18:00:28 +01:00
Anselm Eickhoff
cd0da0f6bf Merge pull request #94 from gardencmp/ergonomic-covalues
Implement queries
2023-09-21 17:31:31 +01:00
19 changed files with 211 additions and 110 deletions

View File

@@ -113,4 +113,4 @@ In the future we'll build a dedicated docs page on the Jazz homepage.
----
Copyright 2023: Garden Computing, Inc.
Copyright 2023 — Garden Computing, Inc.

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-pets",
"private": true,
"version": "0.0.11",
"version": "0.0.14",
"type": "module",
"scripts": {
"dev": "vite",
@@ -16,9 +16,9 @@
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "^0.3.0",
"jazz-react-auth-local": "^0.3.0",
"jazz-react-media-images": "^0.3.0",
"jazz-react": "^0.3.5",
"jazz-react-auth-local": "^0.3.5",
"jazz-react-media-images": "^0.3.5",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.36",
"version": "0.0.39",
"type": "module",
"scripts": {
"dev": "vite",
@@ -16,8 +16,8 @@
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "^0.3.0",
"jazz-react-auth-local": "^0.3.0",
"jazz-react": "^0.3.5",
"jazz-react-auth-local": "^0.3.5",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",

View File

@@ -24,7 +24,7 @@ import { useParams } from "react-router";
/** Walkthrough: Reactively rendering a todo project as a table,
* adding and editing tasks
*
* Here in `<TodoTable/>`, we use `useSyncedData()` for the first time,
* Here in `<TodoTable/>`, we use `useSyncedQuery()` for the first time,
* in this case to load the CoValue for our `TodoProject` as well as
* the `ListOfTasks` referenced in it.
*/
@@ -32,7 +32,7 @@ import { useParams } from "react-router";
export function ProjectTodoTable() {
const projectId = useParams<{ projectId: CoID<TodoProject> }>().projectId;
// `useSyncedData()` reactively subscribes to updates to a CoValue's
// `useSyncedQuery()` reactively subscribes to updates to a CoValue's
// content - whether we create edits locally, load persisted data, or receive
// sync updates from other devices or participants!
// It also recursively resolves and subsribes to all referenced CoValues.

View File

@@ -4,7 +4,7 @@
"types": "src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.3.0",
"version": "0.3.7",
"devDependencies": {
"@types/jest": "^29.5.3",
"@types/ws": "^8.5.5",
@@ -16,8 +16,8 @@
"typescript": "5.0.2"
},
"dependencies": {
"cojson": "^0.3.0",
"cojson-storage-sqlite": "^0.3.0",
"cojson": "^0.3.5",
"cojson-storage-sqlite": "^0.3.7",
"ws": "^8.13.0"
},
"scripts": {

View File

@@ -1,11 +1,11 @@
{
"name": "cojson-storage-indexeddb",
"version": "0.3.0",
"version": "0.3.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.3.0",
"cojson": "^0.3.5",
"typescript": "^5.1.6"
},
"devDependencies": {

View File

@@ -1,13 +1,13 @@
{
"name": "cojson-storage-sqlite",
"type": "module",
"version": "0.3.0",
"version": "0.3.7",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^8.5.2",
"cojson": "^0.3.0",
"cojson": "^0.3.5",
"typescript": "^5.1.6"
},
"scripts": {

View File

@@ -237,19 +237,31 @@ export class SQLiteStorage {
sessions: {},
};
const parsedHeader = (coValueRow?.header &&
JSON.parse(coValueRow.header)) as
| CojsonInternalTypes.CoValueHeader
| undefined;
let parsedHeader;
const newContentPieces: CojsonInternalTypes.NewContentMessage[] = [
{
action: "content",
id: theirKnown.id,
header: theirKnown.header ? undefined : parsedHeader,
new: {},
},
];
try {
parsedHeader = (coValueRow?.header &&
JSON.parse(coValueRow.header)) as
| CojsonInternalTypes.CoValueHeader
| undefined;
} catch (e) {
console.warn(
theirKnown.id,
"Invalid JSON in header",
e,
coValueRow?.header
);
return;
}
const newContentPieces: CojsonInternalTypes.NewContentMessage[] = [
{
action: "content",
id: theirKnown.id,
header: theirKnown.header ? undefined : parsedHeader,
new: {},
},
];
for (const sessionRow of allOurSessions) {
ourKnown.sessions[sessionRow.sessionID] = sessionRow.lastIdx;
@@ -265,7 +277,10 @@ export class SQLiteStorage {
.prepare<[number, number]>(
`SELECT * FROM signatureAfter WHERE ses = ? AND idx >= ?`
)
.all(sessionRow.rowID, firstNewTxIdx) as SignatureAfterRow[];
.all(
sessionRow.rowID,
firstNewTxIdx
) as SignatureAfterRow[];
// console.log(
// theirKnown.id,
@@ -295,7 +310,8 @@ export class SQLiteStorage {
if (!sessionEntry) {
sessionEntry = {
after: idx,
lastSignature: "WILL_BE_REPLACED" as CojsonInternalTypes.Signature,
lastSignature:
"WILL_BE_REPLACED" as CojsonInternalTypes.Signature,
newTransactions: [],
};
newContentPieces[newContentPieces.length - 1]!.new[
@@ -303,7 +319,21 @@ export class SQLiteStorage {
] = sessionEntry;
}
sessionEntry.newTransactions.push(JSON.parse(tx.tx));
let parsedTx;
try {
parsedTx = JSON.parse(tx.tx);
} catch (e) {
console.warn(
theirKnown.id,
"Invalid JSON in transaction",
e,
tx.tx
);
break;
}
sessionEntry.newTransactions.push(parsedTx);
if (
signaturesAndIdxs[0] &&
@@ -331,28 +361,46 @@ export class SQLiteStorage {
const dependedOnCoValues =
parsedHeader?.ruleset.type === "group"
? newContentPieces
.flatMap((piece) => Object.values(piece.new)).flatMap((sessionEntry) =>
sessionEntry.newTransactions.flatMap((tx) => {
if (tx.privacy !== "trusting") return [];
// TODO: avoid parsing here?
return cojsonInternals
.parseJSON(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_")
);
})
)
.flatMap((piece) => Object.values(piece.new))
.flatMap((sessionEntry) =>
sessionEntry.newTransactions.flatMap((tx) => {
if (tx.privacy !== "trusting") return [];
// TODO: avoid parsing here?
let parsedChanges;
try {
parsedChanges = cojsonInternals.parseJSON(
tx.changes
);
} catch (e) {
console.warn(
theirKnown.id,
"Invalid JSON in transaction",
e,
tx.changes
);
return [];
}
return parsedChanges
.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]
: [];
@@ -499,7 +547,7 @@ export class SQLiteStorage {
sessionUpdate.sessionID,
sessionUpdate.lastIdx,
sessionUpdate.lastSignature,
sessionUpdate.bytesSinceLastSignature,
sessionUpdate.bytesSinceLastSignature
) as { rowID: number };
const sessionRowID = upsertedSession.rowID;

View File

@@ -5,7 +5,7 @@
"types": "dist/index.d.ts",
"type": "module",
"license": "MIT",
"version": "0.3.0",
"version": "0.3.5",
"devDependencies": {
"@types/jest": "^29.5.3",
"@typescript-eslint/eslint-plugin": "^6.2.1",

View File

@@ -22,23 +22,27 @@ let blake3incrementalUpdateSLOW_WITH_DEVTOOLS: (
let blake3digestForState: (state: Uint8Array) => Uint8Array;
export const cryptoReady = new Promise<void>((resolve) => {
createBLAKE3().then((bl3) => {
blake3Instance = bl3;
blake3HashOnce = (data) => {
return bl3.init().update(data).digest("binary");
};
blake3HashOnceWithContext = (data, { context }) => {
return bl3.init().update(context).update(data).digest("binary");
};
blake3incrementalUpdateSLOW_WITH_DEVTOOLS = (state, data) => {
bl3.load(state).update(data);
return bl3.save();
};
blake3digestForState = (state) => {
return bl3.load(state).digest("binary");
};
resolve();
});
createBLAKE3()
.then((bl3) => {
blake3Instance = bl3;
blake3HashOnce = (data) => {
return bl3.init().update(data).digest("binary");
};
blake3HashOnceWithContext = (data, { context }) => {
return bl3.init().update(context).update(data).digest("binary");
};
blake3incrementalUpdateSLOW_WITH_DEVTOOLS = (state, data) => {
bl3.load(state).update(data);
return bl3.save();
};
blake3digestForState = (state) => {
return bl3.load(state).digest("binary");
};
resolve();
})
.catch((e) =>
console.error("Failed to load cryptography dependencies", e)
);
});
export type SignerSecret = `signerSecret_z${string}`;

View File

@@ -73,7 +73,24 @@ export function determineValidTransactions(
// console.log("before", { memberState, validTransactions });
const transactor = accountOrAgentIDfromSessionID(sessionID);
const changes = parseJSON(tx.changes);
let changes;
try {
changes = parseJSON(tx.changes);
} catch (e) {
console.warn(
coValue.id,
"Invalid JSON in transaction",
e,
tx,
JSON.stringify(tx.changes, (k, v) =>
k === "changes" || k === "encryptedChanges"
? v.slice(0, 20) + "..."
: v
)
);
continue;
}
const change = changes[0] as
| MapOpPayload<AccountID | AgentID, Role>
@@ -235,7 +252,7 @@ export function determineValidTransactions(
);
} else {
throw new Error(
"Unknown ruleset type " + (coValue.header.ruleset as any).type
"Unknown ruleset type " + (coValue.header.ruleset as {type: string}).type
);
}
}

View File

@@ -9,7 +9,6 @@ import {
WritableStreamDefaultWriter,
} from "isomorphic-streams";
import { RawCoID, SessionID } from "./ids.js";
import { stableStringify } from "./jsonStringify.js";
export type CoValueKnownState = {
id: RawCoID;
@@ -224,7 +223,7 @@ export class SyncManager {
peer.optimisticKnownStates[id] || emptyKnownState(id);
const sendPieces = async () => {
for (const [i, piece] of newContentPieces.entries()) {
for (const [_i, piece] of newContentPieces.entries()) {
// console.log(
// `${id} -> ${peer.id}: Sending content piece ${i + 1}/${newContentPieces.length} header: ${!!piece.header}`,
// // Object.values(piece.new).map((s) => s.newTransactions)
@@ -505,7 +504,11 @@ export class SyncManager {
console.error(
"Failed to add transactions",
msg.id,
newTransactions
JSON.stringify(newTransactions, (k, v) =>
k === "changes" || k === "encryptedChanges"
? v.slice(0, 20) + "..."
: v
)
);
continue;
}

View File

@@ -1,11 +1,11 @@
{
"name": "jazz-browser-auth-local",
"version": "0.3.0",
"version": "0.3.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"jazz-browser": "^0.3.0",
"jazz-browser": "^0.3.5",
"typescript": "^5.1.6"
},
"scripts": {

View File

@@ -1,13 +1,13 @@
{
"name": "jazz-browser-media-images",
"version": "0.3.0",
"version": "0.3.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.3.0",
"cojson": "^0.3.5",
"image-blob-reduce": "^4.1.0",
"jazz-browser": "^0.3.0",
"jazz-browser": "^0.3.5",
"typescript": "^5.1.6"
},
"scripts": {

View File

@@ -1,12 +1,12 @@
{
"name": "jazz-browser",
"version": "0.3.0",
"version": "0.3.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.3.0",
"cojson-storage-indexeddb": "^0.3.0",
"cojson": "^0.3.5",
"cojson-storage-indexeddb": "^0.3.5",
"typescript": "^5.1.6"
},
"scripts": {

View File

@@ -25,7 +25,7 @@ export type BrowserNodeHandle = {
export async function createBrowserNode({
auth,
syncAddress = "wss://sync.jazz.tools",
reconnectionTimeout = 300,
reconnectionTimeout: initialReconnectionTimeout = 500,
}: {
auth: AuthProvider;
syncAddress?: string;
@@ -37,6 +37,15 @@ export async function createBrowserNode({
const firstWsPeer = createWebSocketPeer(syncAddress);
let shouldTryToReconnect = true;
let currentReconnectionTimeout = initialReconnectionTimeout;
function onOnline() {
console.log("Online, resetting reconnection timeout");
currentReconnectionTimeout = initialReconnectionTimeout;
}
window.addEventListener("online", onOnline);
const node = await auth.createNode(
(accountID) => {
const sessionHandle = getSessionHandleFor(accountID);
@@ -53,15 +62,33 @@ export async function createBrowserNode({
peerId.includes(syncAddress)
)
) {
await new Promise((resolve) =>
setTimeout(resolve, reconnectionTimeout)
);
// TODO: this might drain battery, use listeners instead
await new Promise((resolve) => setTimeout(resolve, 100));
} else {
console.log("Websocket disconnected, trying to reconnect");
node.syncManager.addPeer(createWebSocketPeer(syncAddress));
await new Promise((resolve) =>
setTimeout(resolve, reconnectionTimeout)
console.log(
"Websocket disconnected, trying to reconnect in " +
currentReconnectionTimeout +
"ms"
);
currentReconnectionTimeout = Math.min(
currentReconnectionTimeout * 2,
30000
);
await new Promise<void>((resolve) => {
setTimeout(resolve, currentReconnectionTimeout);
window.addEventListener(
"online",
() => {
console.log(
"Online, trying to reconnect immediately"
);
resolve();
},
{ once: true }
);
});
node.syncManager.addPeer(createWebSocketPeer(syncAddress));
}
}
}
@@ -72,6 +99,7 @@ export async function createBrowserNode({
node,
done: () => {
shouldTryToReconnect = false;
window.removeEventListener("online", onOnline);
console.log("Cleaning up node");
for (const peer of Object.values(node.syncManager.peers)) {
peer.outgoing
@@ -292,13 +320,13 @@ function websocketWritableStream<T>(ws: WebSocket) {
}
export function createInviteLink<T extends CoValue>(
value: T | {id: CoID<T>, core: CoValueCore},
value: T | { id: CoID<T>; core: CoValueCore },
role: "reader" | "writer" | "admin",
// default to same address as window.location, but without hash
{
baseURL = window.location.href.replace(/#.*$/, ""),
valueHint
}: { baseURL?: string, valueHint?: string } = {}
valueHint,
}: { baseURL?: string; valueHint?: string } = {}
): string {
const coValueCore = value.core;
const node = coValueCore.node;
@@ -319,7 +347,9 @@ export function createInviteLink<T extends CoValue>(
const inviteSecret = group.createInvite(role);
return `${baseURL}#/invite/${valueHint ? valueHint + "/" : ""}${value.id}/${inviteSecret}`;
return `${baseURL}#/invite/${valueHint ? valueHint + "/" : ""}${
value.id
}/${inviteSecret}`;
}
export function parseInviteLink<C extends CoValue>(
@@ -353,7 +383,6 @@ export function parseInviteLink<C extends CoValue>(
}
return { valueID, inviteSecret, valueHint };
}
}
export function consumeInviteLinkFromWindowLocation<C extends CoValue>(

View File

@@ -1,12 +1,12 @@
{
"name": "jazz-react-auth-local",
"version": "0.3.0",
"version": "0.3.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"jazz-browser-auth-local": "^0.3.0",
"jazz-react": "^0.3.0",
"jazz-browser-auth-local": "^0.3.5",
"jazz-react": "^0.3.5",
"typescript": "^5.1.6"
},
"devDependencies": {

View File

@@ -1,14 +1,14 @@
{
"name": "jazz-react-media-images",
"version": "0.3.0",
"version": "0.3.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.3.0",
"jazz-browser": "^0.3.0",
"jazz-browser-media-images": "^0.3.0",
"jazz-react": "^0.3.0",
"cojson": "^0.3.5",
"jazz-browser": "^0.3.5",
"jazz-browser-media-images": "^0.3.5",
"jazz-react": "^0.3.5",
"typescript": "^5.1.6"
},
"devDependencies": {

View File

@@ -1,12 +1,12 @@
{
"name": "jazz-react",
"version": "0.3.0",
"version": "0.3.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.3.0",
"jazz-browser": "^0.3.0",
"cojson": "^0.3.5",
"jazz-browser": "^0.3.5",
"typescript": "^5.1.6"
},
"devDependencies": {