Compare commits
32 Commits
cojson@0.1
...
jazz-react
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ef5b6b2ab | ||
|
|
1384ebed84 | ||
|
|
17e53f9998 | ||
|
|
cfb1f39efe | ||
|
|
2234276dcf | ||
|
|
bb0a6a0600 | ||
|
|
0a6eb0c10a | ||
|
|
88b67d89e0 | ||
|
|
1a65d826b2 | ||
|
|
6c65ec2b46 | ||
|
|
5b578a832d | ||
|
|
042afc52d7 | ||
|
|
1b83493964 | ||
|
|
3b50da1a74 | ||
|
|
8e0fc74d9f | ||
|
|
e28326f32c | ||
|
|
d7e8b0b9da | ||
|
|
c46a1f6b0a | ||
|
|
7947918278 | ||
|
|
50c36e7255 | ||
|
|
c39a7ed1b7 | ||
|
|
83762dbb0f | ||
|
|
7c82e12508 | ||
|
|
6db149be36 | ||
|
|
909a101f99 | ||
|
|
df0b6fe138 | ||
|
|
0543756016 | ||
|
|
92eae0e180 | ||
|
|
9ccc97fcd3 | ||
|
|
120ba57274 | ||
|
|
0679a64002 | ||
|
|
e9d561adbd |
63
.github/workflows/build-and-deploy.yaml
vendored
63
.github/workflows/build-and-deploy.yaml
vendored
@@ -7,8 +7,11 @@ on:
|
|||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
example: ["todo", "pets"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -17,40 +20,50 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: yarn.lock
|
cache-dependency-path: yarn.lock
|
||||||
- name: Nuke Workspace
|
|
||||||
run: |
|
|
||||||
rm package.json yarn.lock;
|
|
||||||
- name: Yarn Build
|
|
||||||
run: |
|
|
||||||
yarn install --frozen-lockfile;
|
|
||||||
yarn build;
|
|
||||||
working-directory: ./examples/todo
|
|
||||||
|
|
||||||
- uses: satackey/action-docker-layer-caching@v0.0.11
|
- name: Set up Docker Buildx
|
||||||
continue-on-error: true
|
uses: docker/setup-buildx-action@v2
|
||||||
with:
|
|
||||||
key: docker-layer-caching-${{ github.workflow }}-{hash}
|
|
||||||
restore-keys: |
|
|
||||||
docker-layer-caching-${{ github.workflow }}-
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: gardencmp
|
username: gardencmp
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Docker Build & Push
|
- name: Nuke Workspace
|
||||||
run: |
|
run: |
|
||||||
export DOCKER_TAG=ghcr.io/gardencmp/jazz-example-todo:${{github.head_ref || github.ref_name}}-${{github.sha}}-$(date +%s) ;
|
rm package.json yarn.lock;
|
||||||
docker build . --file Dockerfile --tag $DOCKER_TAG;
|
|
||||||
docker push $DOCKER_TAG;
|
|
||||||
echo "DOCKER_TAG=$DOCKER_TAG" >> $GITHUB_ENV
|
|
||||||
working-directory: ./examples/todo
|
|
||||||
|
|
||||||
|
- name: Yarn Build
|
||||||
|
run: |
|
||||||
|
yarn install --frozen-lockfile;
|
||||||
|
yarn build;
|
||||||
|
working-directory: ./examples/${{ matrix.example }}
|
||||||
|
|
||||||
|
- name: Docker Build & Push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: ./examples/${{ matrix.example }}
|
||||||
|
push: true
|
||||||
|
tags: ghcr.io/gardencmp/${{github.event.repository.name}}-example-${{ matrix.example }}:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
example: ["todo", "pets"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
- uses: gacts/install-nomad@v1
|
- uses: gacts/install-nomad@v1
|
||||||
- name: Tailscale
|
- name: Tailscale
|
||||||
uses: tailscale/github-action@v1
|
uses: tailscale/github-action@v1
|
||||||
@@ -69,9 +82,9 @@ jobs:
|
|||||||
|
|
||||||
export DOCKER_USER=gardencmp;
|
export DOCKER_USER=gardencmp;
|
||||||
export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
|
export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
|
||||||
export DOCKER_TAG=${{ env.DOCKER_TAG }};
|
export DOCKER_TAG=ghcr.io/gardencmp/${{github.event.repository.name}}-example-${{ matrix.example }}:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}};
|
||||||
|
|
||||||
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
|
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
|
||||||
cat job-instance.nomad;
|
cat job-instance.nomad;
|
||||||
NOMAD_ADDR='http://control1v2-london:4646' nomad job run job-instance.nomad;
|
NOMAD_ADDR='http://control1v2-london:4646' nomad job run job-instance.nomad;
|
||||||
working-directory: ./examples/todo
|
working-directory: ./examples/${{ matrix.example }}
|
||||||
292
DOCS.md
292
DOCS.md
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## `Media` (namespace in `cojson`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export Media
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
TODO: doc generator not implemented yet
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
## `LocalNode` (class in `cojson`)
|
## `LocalNode` (class in `cojson`)
|
||||||
@@ -340,7 +351,7 @@ Creates an invite for new members to indirectly join the group, allowing them to
|
|||||||
<summary><code>group.createMap(meta)</code> </summary>
|
<summary><code>group.createMap(meta)</code> </summary>
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
group.createMap<M extends CoMap<{ [key: string]: JsonValue }, null | JsonObject>>(
|
group.createMap<M extends CoMap<{ [key: string]: JsonValue | undefined }, null | JsonObject>>(
|
||||||
meta: M["meta"]
|
meta: M["meta"]
|
||||||
): M
|
): M
|
||||||
```
|
```
|
||||||
@@ -405,7 +416,7 @@ TODO: document
|
|||||||
## `CoMap` (class in `cojson`)
|
## `CoMap` (class in `cojson`)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export class CoMap<M extends { [key: string]: JsonValue }, Meta extends JsonObject | null> implements ReadableCoValue {...}
|
export class CoMap<M extends { [key: string]: JsonValue | undefined }, Meta extends JsonObject | null> implements ReadableCoValue {...}
|
||||||
```
|
```
|
||||||
A collaborative map with precise shape `M` and optional static metadata `Meta`
|
A collaborative map with precise shape `M` and optional static metadata `Meta`
|
||||||
|
|
||||||
@@ -421,7 +432,7 @@ A collaborative map with precise shape `M` and optional static metadata `Meta`
|
|||||||
<summary><code>coMap.id</code> </summary>
|
<summary><code>coMap.id</code> </summary>
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
coMap.id: CoID<CoMap<MapM<M>, Meta>>
|
coMap.id: CoID<CoMap<M, Meta>>
|
||||||
```
|
```
|
||||||
The `CoValue`'s (precisely typed) `CoID`
|
The `CoValue`'s (precisely typed) `CoID`
|
||||||
|
|
||||||
@@ -655,7 +666,7 @@ Lets you apply edits to a `CoValue`, inside the changer callback, which receives
|
|||||||
## `WriteableCoMap` (class in `cojson`)
|
## `WriteableCoMap` (class in `cojson`)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export class WriteableCoMap<M extends { [key: string]: JsonValue }, Meta extends JsonObject | null> extends CoMap<M, Meta> implements WriteableCoValue {...}
|
export class WriteableCoMap<M extends { [key: string]: JsonValue | undefined }, Meta extends JsonObject | null> extends CoMap<M, Meta> implements WriteableCoValue {...}
|
||||||
```
|
```
|
||||||
A collaborative map with precise shape `M` and optional static metadata `Meta`
|
A collaborative map with precise shape `M` and optional static metadata `Meta`
|
||||||
|
|
||||||
@@ -671,7 +682,7 @@ A collaborative map with precise shape `M` and optional static metadata `Meta`
|
|||||||
<summary><code>writeableCoMap.id</code> (from <code>CoMap</code>) </summary>
|
<summary><code>writeableCoMap.id</code> (from <code>CoMap</code>) </summary>
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
writeableCoMap.id: CoID<CoMap<MapM<M>, Meta>>
|
writeableCoMap.id: CoID<CoMap<M, Meta>>
|
||||||
```
|
```
|
||||||
The `CoValue`'s (precisely typed) `CoID`
|
The `CoValue`'s (precisely typed) `CoID`
|
||||||
|
|
||||||
@@ -1574,7 +1585,7 @@ TODO: document
|
|||||||
<summary><code>coStream.items</code> (undocumented)</summary>
|
<summary><code>coStream.items</code> (undocumented)</summary>
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
coStream.items: { [key: SessionID]: T[] }
|
coStream.items: { [key: SessionID]: {item: T, madeAt: number}[] }
|
||||||
```
|
```
|
||||||
TODO: document
|
TODO: document
|
||||||
|
|
||||||
@@ -1628,6 +1639,44 @@ TODO: document
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>coStream.getLastItemsPerAccount()</code> (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
coStream.getLastItemsPerAccount(): { [account: AccountID]: T | undefined }
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>coStream.getLastItemFrom(account)</code> (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
coStream.getLastItemFrom(
|
||||||
|
account: AccountID
|
||||||
|
): undefined | T
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>coStream.getLastItemFromMe()</code> (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
coStream.getLastItemFromMe(): undefined | T
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><code>coStream.toJSON()</code> </summary>
|
<summary><code>coStream.toJSON()</code> </summary>
|
||||||
|
|
||||||
@@ -1755,7 +1804,7 @@ TODO: document
|
|||||||
<summary><code>writeableCoStream.items</code> (from <code>CoStream</code>) (undocumented)</summary>
|
<summary><code>writeableCoStream.items</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
writeableCoStream.items: { [key: SessionID]: T[] }
|
writeableCoStream.items: { [key: SessionID]: {item: T, madeAt: number}[] }
|
||||||
```
|
```
|
||||||
TODO: document
|
TODO: document
|
||||||
|
|
||||||
@@ -1826,6 +1875,44 @@ TODO: document
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>writeableCoStream.getLastItemsPerAccount()</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
writeableCoStream.getLastItemsPerAccount(): { [account: AccountID]: T | undefined }
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>writeableCoStream.getLastItemFrom(account)</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
writeableCoStream.getLastItemFrom(
|
||||||
|
account: AccountID
|
||||||
|
): undefined | T
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>writeableCoStream.getLastItemFromMe()</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
writeableCoStream.getLastItemFromMe(): undefined | T
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><code>writeableCoStream.toJSON()</code> (from <code>CoStream</code>) </summary>
|
<summary><code>writeableCoStream.toJSON()</code> (from <code>CoStream</code>) </summary>
|
||||||
|
|
||||||
@@ -1933,7 +2020,7 @@ TODO: document
|
|||||||
<summary><code>binaryCoStream.items</code> (from <code>CoStream</code>) (undocumented)</summary>
|
<summary><code>binaryCoStream.items</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
binaryCoStream.items: { [key: SessionID]: T[] }
|
binaryCoStream.items: { [key: SessionID]: {item: T, madeAt: number}[] }
|
||||||
```
|
```
|
||||||
TODO: document
|
TODO: document
|
||||||
|
|
||||||
@@ -1974,10 +2061,12 @@ The `Group` this `CoValue` belongs to (determining permissions)
|
|||||||
### Methods
|
### Methods
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><code>binaryCoStream.getBinaryChunks()</code> (undocumented)</summary>
|
<summary><code>binaryCoStream.getBinaryChunks(allowUnfinished)</code> (undocumented)</summary>
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
binaryCoStream.getBinaryChunks(): undefined | BinaryChunkInfo & {chunks: Uint8Array[], finished: boolean}
|
binaryCoStream.getBinaryChunks(
|
||||||
|
allowUnfinished: boolean
|
||||||
|
): undefined | BinaryChunkInfo & {chunks: Uint8Array[], finished: boolean}
|
||||||
```
|
```
|
||||||
TODO: document
|
TODO: document
|
||||||
|
|
||||||
@@ -2019,6 +2108,44 @@ TODO: document
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>binaryCoStream.getLastItemsPerAccount()</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
binaryCoStream.getLastItemsPerAccount(): { [account: AccountID]: T | undefined }
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>binaryCoStream.getLastItemFrom(account)</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
binaryCoStream.getLastItemFrom(
|
||||||
|
account: AccountID
|
||||||
|
): undefined | BinaryStreamItem
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>binaryCoStream.getLastItemFromMe()</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
binaryCoStream.getLastItemFromMe(): undefined | BinaryStreamItem
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><code>binaryCoStream.toJSON()</code> (from <code>CoStream</code>) </summary>
|
<summary><code>binaryCoStream.toJSON()</code> (from <code>CoStream</code>) </summary>
|
||||||
|
|
||||||
@@ -2126,7 +2253,7 @@ TODO: document
|
|||||||
<summary><code>writeableBinaryCoStream.items</code> (from <code>BinaryCoStream</code>) (undocumented)</summary>
|
<summary><code>writeableBinaryCoStream.items</code> (from <code>BinaryCoStream</code>) (undocumented)</summary>
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
writeableBinaryCoStream.items: { [key: SessionID]: T[] }
|
writeableBinaryCoStream.items: { [key: SessionID]: {item: T, madeAt: number}[] }
|
||||||
```
|
```
|
||||||
TODO: document
|
TODO: document
|
||||||
|
|
||||||
@@ -2215,10 +2342,12 @@ TODO: document
|
|||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><code>writeableBinaryCoStream.getBinaryChunks()</code> (from <code>BinaryCoStream</code>) (undocumented)</summary>
|
<summary><code>writeableBinaryCoStream.getBinaryChunks(allowUnfinished)</code> (from <code>BinaryCoStream</code>) (undocumented)</summary>
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
writeableBinaryCoStream.getBinaryChunks(): undefined | BinaryChunkInfo & {chunks: Uint8Array[], finished: boolean}
|
writeableBinaryCoStream.getBinaryChunks(
|
||||||
|
allowUnfinished: boolean
|
||||||
|
): undefined | BinaryChunkInfo & {chunks: Uint8Array[], finished: boolean}
|
||||||
```
|
```
|
||||||
TODO: document
|
TODO: document
|
||||||
|
|
||||||
@@ -2240,6 +2369,44 @@ TODO: document
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>writeableBinaryCoStream.getLastItemsPerAccount()</code> (from <code>BinaryCoStream</code>) (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
writeableBinaryCoStream.getLastItemsPerAccount(): { [account: AccountID]: T | undefined }
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>writeableBinaryCoStream.getLastItemFrom(account)</code> (from <code>BinaryCoStream</code>) (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
writeableBinaryCoStream.getLastItemFrom(
|
||||||
|
account: AccountID
|
||||||
|
): undefined | BinaryStreamItem
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>writeableBinaryCoStream.getLastItemFromMe()</code> (from <code>BinaryCoStream</code>) (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
writeableBinaryCoStream.getLastItemFromMe(): undefined | BinaryStreamItem
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><code>writeableBinaryCoStream.toJSON()</code> (from <code>BinaryCoStream</code>) </summary>
|
<summary><code>writeableBinaryCoStream.toJSON()</code> (from <code>BinaryCoStream</code>) </summary>
|
||||||
|
|
||||||
@@ -2365,6 +2532,18 @@ TODO: document
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>coValueCore._decryptionCache</code> (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
coValueCore._decryptionCache: { [key: Encrypted<JsonValue[], JsonValue>]: Stringified<JsonValue[]> | undefined }
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><code>coValueCore._cachedContent</code> (undocumented)</summary>
|
<summary><code>coValueCore._cachedContent</code> (undocumented)</summary>
|
||||||
|
|
||||||
@@ -2461,6 +2640,41 @@ TODO: document
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>coValueCore.tryAddTransactionsAsync(sessionID, newTransactions, givenExpectedNewHash, newSignature)</code> (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
coValueCore.tryAddTransactionsAsync(
|
||||||
|
sessionID: SessionID,
|
||||||
|
newTransactions: Transaction[],
|
||||||
|
givenExpectedNewHash: undefined | TEMPLATE_LITERAL,
|
||||||
|
newSignature: TEMPLATE_LITERAL
|
||||||
|
): Promise<boolean>
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>coValueCore.doAddTransactions(sessionID, newTransactions, newSignature, expectedNewHash, newStreamingHash)</code> (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
coValueCore.doAddTransactions(
|
||||||
|
sessionID: SessionID,
|
||||||
|
newTransactions: Transaction[],
|
||||||
|
newSignature: TEMPLATE_LITERAL,
|
||||||
|
expectedNewHash: TEMPLATE_LITERAL,
|
||||||
|
newStreamingHash: StreamingHash
|
||||||
|
): void
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><code>coValueCore.subscribe(listener)</code> (undocumented)</summary>
|
<summary><code>coValueCore.subscribe(listener)</code> (undocumented)</summary>
|
||||||
|
|
||||||
@@ -2490,6 +2704,21 @@ TODO: document
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><code>coValueCore.expectedNewHashAfterAsync(sessionID, newTransactions)</code> (undocumented)</summary>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
coValueCore.expectedNewHashAfterAsync(
|
||||||
|
sessionID: SessionID,
|
||||||
|
newTransactions: Transaction[]
|
||||||
|
): Promise<{expectedNewHash: TEMPLATE_LITERAL, newStreamingHash: StreamingHash}>
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><code>coValueCore.makeTransaction(changes, privacy)</code> (undocumented)</summary>
|
<summary><code>coValueCore.makeTransaction(changes, privacy)</code> (undocumented)</summary>
|
||||||
|
|
||||||
@@ -2587,7 +2816,7 @@ TODO: document
|
|||||||
```typescript
|
```typescript
|
||||||
coValueCore.newContentSince(
|
coValueCore.newContentSince(
|
||||||
knownState: undefined | CoValueKnownState
|
knownState: undefined | CoValueKnownState
|
||||||
): undefined | NewContentMessage
|
): undefined | NewContentMessage[]
|
||||||
```
|
```
|
||||||
TODO: document
|
TODO: document
|
||||||
|
|
||||||
@@ -2917,7 +3146,7 @@ TODO: doc generator not implemented yet
|
|||||||
## `CoValueImpl` (type alias in `cojson`)
|
## `CoValueImpl` (type alias in `cojson`)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export type CoValueImpl = CoMap<{ [key: string]: JsonValue }, JsonObject | null> | CoList<JsonValue, JsonObject | null> | CoStream<JsonValue, JsonObject | null> | BinaryCoStream<BinaryCoStreamMeta> | Static<JsonObject>
|
export type CoValueImpl = CoMap<{ [key: string]: JsonValue | undefined }, JsonObject | null> | CoList<JsonValue, JsonObject | null> | CoStream<JsonValue, JsonObject | null> | BinaryCoStream<BinaryCoStreamMeta> | Static<JsonObject>
|
||||||
```
|
```
|
||||||
TODO: document
|
TODO: document
|
||||||
|
|
||||||
@@ -3033,6 +3262,28 @@ TODO: document
|
|||||||
|
|
||||||
TODO: doc generator not implemented yet
|
TODO: doc generator not implemented yet
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## `cojsonReady` (variabl in `cojson`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export cojsonReady
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
TODO: doc generator not implemented yet
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## `MAX_RECOMMENDED_TX_SIZE` (variabl in `cojson`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export MAX_RECOMMENDED_TX_SIZE
|
||||||
|
```
|
||||||
|
TODO: document
|
||||||
|
|
||||||
|
TODO: doc generator not implemented yet
|
||||||
|
|
||||||
|
|
||||||
# jazz-react
|
# jazz-react
|
||||||
|
|
||||||
@@ -3102,17 +3353,6 @@ TODO: doc generator not implemented yet
|
|||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
## `useCreateBinaryStreamHandler(onCreated, inGroup, meta)` (function in `jazz-react`)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export function useCreateBinaryStreamHandler(onCreated: (createdStream: C) => void, inGroup: Group, meta: C["meta"]): (event: ChangeEvent) => void
|
|
||||||
```
|
|
||||||
TODO: document
|
|
||||||
|
|
||||||
TODO: doc generator not implemented yet
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
## `createInviteLink(value, role, {baseURL?})` (function in `jazz-react`)
|
## `createInviteLink(value, role, {baseURL?})` (function in `jazz-react`)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|||||||
18
examples/pets/.eslintrc.cjs
Normal file
18
examples/pets/.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
24
examples/pets/.gitignore
vendored
Normal file
24
examples/pets/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
4
examples/pets/Dockerfile
Normal file
4
examples/pets/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
FROM caddy:2.7.3-alpine
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/gardencmp/jazz"
|
||||||
|
|
||||||
|
COPY ./dist /usr/share/caddy/
|
||||||
51
examples/pets/README.md
Normal file
51
examples/pets/README.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Jazz Rate-My-Pet List Example
|
||||||
|
|
||||||
|
Live version: https://example-pets.jazz.tools
|
||||||
|
|
||||||
|
## Installing & running the example locally
|
||||||
|
|
||||||
|
Start by checking out just the example app to a folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx degit gardencmp/jazz/examples/pets jazz-example-pets
|
||||||
|
cd jazz-example-pets
|
||||||
|
```
|
||||||
|
|
||||||
|
(This ensures that you have the example app without git history or our multi-package monorepo)
|
||||||
|
|
||||||
|
Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the dev server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Walkthrough
|
||||||
|
|
||||||
|
### Main parts
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
### Helpers
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Questions / problems / feedback
|
||||||
|
|
||||||
|
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
|
||||||
|
|
||||||
|
|
||||||
|
## Configuration: sync server
|
||||||
|
|
||||||
|
By default, the example app uses [Jazz Global Mesh](https://jazz.tools/mesh) (`wss://sync.jazz.tools`) - so cross-device use, invites and collaboration should just work.
|
||||||
|
|
||||||
|
You can also run a local sync server by running `npx cojson-simple-sync` and adding the query param `?sync=ws://localhost:4200` to the URL of the example app (for example: `http://localhost:5173/?sync=ws://localhost:4200`), or by setting the `sync` parameter of the `<WithJazz>` provider component in [./src/0_main.tsx](./src/0_main.tsx).
|
||||||
16
examples/pets/components.json
Normal file
16
examples/pets/components.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "stone",
|
||||||
|
"cssVariables": true
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/basicComponents",
|
||||||
|
"utils": "@/basicComponents/lib/utils"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
examples/pets/index.html
Normal file
13
examples/pets/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" href="/jazz-logo.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Jazz Rate My Pet Example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/0_main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
56
examples/pets/job-template.nomad
Normal file
56
examples/pets/job-template.nomad
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
job "example-pets$BRANCH_SUFFIX" {
|
||||||
|
region = "global"
|
||||||
|
datacenters = ["*"]
|
||||||
|
|
||||||
|
group "static" {
|
||||||
|
count = 8
|
||||||
|
|
||||||
|
network {
|
||||||
|
port "http" {
|
||||||
|
to = 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constraint {
|
||||||
|
attribute = "${node.class}"
|
||||||
|
operator = "="
|
||||||
|
value = "mesh"
|
||||||
|
}
|
||||||
|
|
||||||
|
spread {
|
||||||
|
attribute = "${node.datacenter}"
|
||||||
|
weight = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
constraint {
|
||||||
|
distinct_hosts = true
|
||||||
|
}
|
||||||
|
|
||||||
|
task "server" {
|
||||||
|
driver = "docker"
|
||||||
|
|
||||||
|
config {
|
||||||
|
image = "$DOCKER_TAG"
|
||||||
|
ports = ["http"]
|
||||||
|
|
||||||
|
auth = {
|
||||||
|
username = "$DOCKER_USER"
|
||||||
|
password = "$DOCKER_PASSWORD"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service {
|
||||||
|
tags = ["public"]
|
||||||
|
name = "example-pets$BRANCH_SUFFIX"
|
||||||
|
port = "http"
|
||||||
|
provider = "consul"
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
cpu = 50 # MHz
|
||||||
|
memory = 50 # MB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# deploy bump 4
|
||||||
45
examples/pets/package.json
Normal file
45
examples/pets/package.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "jazz-example-pets",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.6",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-toast": "^1.1.4",
|
||||||
|
"@types/qrcode": "^1.5.1",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"jazz-react": "^0.2.2",
|
||||||
|
"jazz-react-auth-local": "^0.2.2",
|
||||||
|
"jazz-react-media-images": "^0.2.2",
|
||||||
|
"lucide-react": "^0.274.0",
|
||||||
|
"qrcode": "^1.5.3",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"tailwind-merge": "^1.14.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"uniqolor": "^1.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"postcss": "^8.4.27",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
examples/pets/postcss.config.js
Normal file
6
examples/pets/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
BIN
examples/pets/public/jazz-logo.png
Normal file
BIN
examples/pets/public/jazz-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
38
examples/pets/src/0_main.tsx
Normal file
38
examples/pets/src/0_main.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
import { WithJazz } from "jazz-react";
|
||||||
|
import { LocalAuth } from "jazz-react-auth-local";
|
||||||
|
|
||||||
|
import { ThemeProvider, TitleAndLogo } from "./basicComponents/index.ts";
|
||||||
|
import { PrettyAuthUI } from "./components/Auth.tsx";
|
||||||
|
import App from "./2_App.tsx";
|
||||||
|
|
||||||
|
/** Walkthrough: The top-level provider `<WithJazz/>`
|
||||||
|
*
|
||||||
|
* This shows how to use the top-level provider `<WithJazz/>`,
|
||||||
|
* which provides the rest of the app with a `LocalNode` (used through `useJazz` later),
|
||||||
|
* based on `LocalAuth` that uses PassKeys (aka WebAuthn) to store a user's account secret
|
||||||
|
* - no backend needed. */
|
||||||
|
|
||||||
|
const appName = "Jazz Rate My Pet Example";
|
||||||
|
|
||||||
|
const auth = LocalAuth({
|
||||||
|
appName,
|
||||||
|
Component: PrettyAuthUI,
|
||||||
|
});
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ThemeProvider>
|
||||||
|
<TitleAndLogo name={appName} />
|
||||||
|
|
||||||
|
<WithJazz auth={auth}>
|
||||||
|
<App />
|
||||||
|
</WithJazz>
|
||||||
|
</ThemeProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Walkthrough: Continue with ./1_types.ts */
|
||||||
29
examples/pets/src/1_types.ts
Normal file
29
examples/pets/src/1_types.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { CoMap, CoID, CoStream, Media } from "cojson";
|
||||||
|
|
||||||
|
/** Walkthrough: Defining the data model with CoJSON
|
||||||
|
*
|
||||||
|
* Here, we define our main data model of TODO
|
||||||
|
*
|
||||||
|
* TODO
|
||||||
|
**/
|
||||||
|
|
||||||
|
export type PetPost = CoMap<{
|
||||||
|
name: string;
|
||||||
|
image: CoID<Media.ImageDefinition>;
|
||||||
|
reactions: CoID<PetReactions>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const REACTION_TYPES = [
|
||||||
|
"aww",
|
||||||
|
"love",
|
||||||
|
"haha",
|
||||||
|
"wow",
|
||||||
|
"tiny",
|
||||||
|
"chonkers",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type ReactionType = (typeof REACTION_TYPES)[number];
|
||||||
|
|
||||||
|
export type PetReactions = CoStream<ReactionType>;
|
||||||
|
|
||||||
|
/** Walkthrough: Continue with ./2_App.tsx */
|
||||||
48
examples/pets/src/2_App.tsx
Normal file
48
examples/pets/src/2_App.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { useJazz } from "jazz-react";
|
||||||
|
|
||||||
|
import { PetPost } from "./1_types";
|
||||||
|
|
||||||
|
import { Button } from "./basicComponents";
|
||||||
|
|
||||||
|
import { useSimpleHashRouterThatAcceptsInvites } from "./router";
|
||||||
|
import { RatePetPostUI } from "./4_RatePetPostUI";
|
||||||
|
import { CreatePetPostForm } from "./3_CreatePetPostForm";
|
||||||
|
|
||||||
|
/** Walkthrough: Creating pet posts & routing in `<App/>`
|
||||||
|
*
|
||||||
|
* <App> is the main app component, handling client-side routing based
|
||||||
|
* on the CoValue ID (CoID) of our PetPost, stored in the URL hash
|
||||||
|
* - which can also contain invite links.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
// A `LocalNode` represents a local view of loaded & created CoValues.
|
||||||
|
// It is associated with a current user account, which will determine
|
||||||
|
// access rights to CoValues. We get it from the top-level provider `<WithJazz/>`.
|
||||||
|
const { localNode, logOut } = useJazz();
|
||||||
|
|
||||||
|
// This sets up routing and accepting invites, skip for now
|
||||||
|
const [currentPetPostID, navigateToPetPostID] =
|
||||||
|
useSimpleHashRouterThatAcceptsInvites<PetPost>(localNode);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full items-center justify-start gap-10 pt-10 pb-10 px-5">
|
||||||
|
{currentPetPostID ? (
|
||||||
|
<RatePetPostUI petPostID={currentPetPostID} />
|
||||||
|
) : (
|
||||||
|
<CreatePetPostForm onCreate={navigateToPetPostID} />
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
navigateToPetPostID(undefined);
|
||||||
|
logOut();
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Log Out
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Walkthrough: continue with ./3_CreatePetPostForm.tsx */
|
||||||
103
examples/pets/src/3_CreatePetPostForm.tsx
Normal file
103
examples/pets/src/3_CreatePetPostForm.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { ChangeEvent, useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import { CoID } from "cojson";
|
||||||
|
import { useJazz, useTelepathicState } from "jazz-react";
|
||||||
|
import { createImage } from "jazz-browser-media-images";
|
||||||
|
|
||||||
|
import { PetPost, PetReactions } from "./1_types";
|
||||||
|
|
||||||
|
import { Input, Button } from "./basicComponents";
|
||||||
|
import { useLoadImage } from "jazz-react-media-images";
|
||||||
|
|
||||||
|
/** Walkthrough: TODO
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function CreatePetPostForm({
|
||||||
|
onCreate,
|
||||||
|
}: {
|
||||||
|
onCreate: (id: CoID<PetPost>) => void;
|
||||||
|
}) {
|
||||||
|
const { localNode } = useJazz();
|
||||||
|
|
||||||
|
const [newPostId, setNewPostId] = useState<CoID<PetPost> | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const newPetPost = useTelepathicState(newPostId);
|
||||||
|
|
||||||
|
const onChangeName = useCallback(
|
||||||
|
(name: string) => {
|
||||||
|
let petPost = newPetPost;
|
||||||
|
if (!petPost) {
|
||||||
|
const petPostGroup = localNode.createGroup();
|
||||||
|
petPost = petPostGroup.createMap<PetPost>();
|
||||||
|
const petReactions = petPostGroup.createStream<PetReactions>();
|
||||||
|
|
||||||
|
petPost = petPost.edit((petPost) => {
|
||||||
|
petPost.set("reactions", petReactions.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
setNewPostId(petPost.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
petPost.edit((petPost) => {
|
||||||
|
petPost.set("name", name);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[localNode, newPetPost]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onImageSelected = useCallback(
|
||||||
|
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!newPetPost || !event.target.files) return;
|
||||||
|
|
||||||
|
const imageDefinition = await createImage(
|
||||||
|
event.target.files[0],
|
||||||
|
newPetPost.group
|
||||||
|
);
|
||||||
|
|
||||||
|
newPetPost.edit((petPost) => {
|
||||||
|
petPost.set("image", imageDefinition.id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[newPetPost]
|
||||||
|
);
|
||||||
|
|
||||||
|
const petImage = useLoadImage(newPetPost?.get("image"));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-10">
|
||||||
|
<p>Share your pet with friends!</p>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Pet Name"
|
||||||
|
className="text-3xl py-6"
|
||||||
|
onChange={(event) => onChangeName(event.target.value)}
|
||||||
|
value={newPetPost?.get("name") || ""}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{petImage ? (
|
||||||
|
<img
|
||||||
|
className="w-80 max-w-full rounded"
|
||||||
|
src={petImage.highestResSrc || petImage.placeholderDataURL}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
disabled={!newPetPost?.get("name")}
|
||||||
|
onChange={onImageSelected}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{newPetPost?.get("name") && newPetPost?.get("image") && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
onCreate(newPetPost.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Submit Post
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
103
examples/pets/src/4_RatePetPostUI.tsx
Normal file
103
examples/pets/src/4_RatePetPostUI.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { AccountID, CoID } from "cojson";
|
||||||
|
import { useTelepathicState } from "jazz-react";
|
||||||
|
|
||||||
|
import { PetPost, PetReactions, ReactionType, REACTION_TYPES } from "./1_types";
|
||||||
|
|
||||||
|
import { ShareButton } from "./components/ShareButton";
|
||||||
|
import { NameBadge } from "./components/NameBadge";
|
||||||
|
import { Button } from "./basicComponents";
|
||||||
|
import { useLoadImage } from "jazz-react-media-images";
|
||||||
|
|
||||||
|
/** Walkthrough: TODO
|
||||||
|
*/
|
||||||
|
|
||||||
|
const reactionEmojiMap: { [reaction in ReactionType]: string } = {
|
||||||
|
aww: "😍",
|
||||||
|
love: "❤️",
|
||||||
|
haha: "😂",
|
||||||
|
wow: "😮",
|
||||||
|
tiny: "🐥",
|
||||||
|
chonkers: "🐘",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RatePetPostUI({ petPostID }: { petPostID: CoID<PetPost> }) {
|
||||||
|
const petPost = useTelepathicState(petPostID);
|
||||||
|
const petReactions = useTelepathicState(petPost?.get("reactions"));
|
||||||
|
const petImage = useLoadImage(petPost?.get("image"));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<h1 className="text-3xl font-bold">{petPost?.get("name")}</h1>
|
||||||
|
<ShareButton petPost={petPost} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{petImage && (
|
||||||
|
<img
|
||||||
|
className="w-80 max-w-full rounded"
|
||||||
|
src={petImage.highestResSrc || petImage.placeholderDataURL}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-between max-w-xs flex-wrap">
|
||||||
|
{REACTION_TYPES.map((reactionType) => (
|
||||||
|
<Button
|
||||||
|
key={reactionType}
|
||||||
|
variant={
|
||||||
|
petReactions?.getLastItemFromMe() === reactionType
|
||||||
|
? "default"
|
||||||
|
: "outline"
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
petReactions?.edit((reactions) => {
|
||||||
|
reactions.push(reactionType);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
title={`React with ${reactionType}`}
|
||||||
|
className="text-2xl px-2"
|
||||||
|
>
|
||||||
|
{reactionEmojiMap[reactionType]}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{petPost?.group.myRole() === "admin" && petReactions && (
|
||||||
|
<ReactionOverview petReactions={petReactions} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReactionOverview({ petReactions }: { petReactions: PetReactions }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Reactions</h2>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{REACTION_TYPES.map((reactionType) => {
|
||||||
|
const accountsWithThisReaction = Object.entries(
|
||||||
|
petReactions.getLastItemsPerAccount()
|
||||||
|
).flatMap(([accountID, reaction]) =>
|
||||||
|
reaction === reactionType ? [accountID] : []
|
||||||
|
);
|
||||||
|
|
||||||
|
if (accountsWithThisReaction.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
key={reactionType}
|
||||||
|
>
|
||||||
|
{reactionEmojiMap[reactionType]}{" "}
|
||||||
|
{accountsWithThisReaction.map((accountID) => (
|
||||||
|
<NameBadge
|
||||||
|
key={accountID}
|
||||||
|
accountID={accountID as AccountID}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
examples/pets/src/basicComponents/TitleAndLogo.tsx
Normal file
12
examples/pets/src/basicComponents/TitleAndLogo.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Toaster } from ".";
|
||||||
|
|
||||||
|
export function TitleAndLogo({ name }: { name: string }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2 justify-center mt-5">
|
||||||
|
<img src="jazz-logo.png" className="h-5" /> {name}
|
||||||
|
</div>
|
||||||
|
<Toaster />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
examples/pets/src/basicComponents/index.ts
Normal file
7
examples/pets/src/basicComponents/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export { Button } from "./ui/button";
|
||||||
|
export { Input } from "./ui/input";
|
||||||
|
export { Toaster } from "./ui/toaster";
|
||||||
|
export { useToast } from "./ui/use-toast";
|
||||||
|
export { Skeleton } from "./ui/skeleton";
|
||||||
|
export { TitleAndLogo } from "./TitleAndLogo";
|
||||||
|
export { ThemeProvider } from "./themeProvider";
|
||||||
6
examples/pets/src/basicComponents/lib/utils.ts
Normal file
6
examples/pets/src/basicComponents/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
72
examples/pets/src/basicComponents/themeProvider.tsx
Normal file
72
examples/pets/src/basicComponents/themeProvider.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type ThemeProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
defaultTheme?: string;
|
||||||
|
storageKey?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ThemeProviderState = {
|
||||||
|
theme: string;
|
||||||
|
setTheme: (theme: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
theme: "system",
|
||||||
|
setTheme: () => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
defaultTheme = "system",
|
||||||
|
storageKey = "vite-ui-theme",
|
||||||
|
...props
|
||||||
|
}: ThemeProviderProps) {
|
||||||
|
const [theme, setTheme] = useState(
|
||||||
|
() => localStorage.getItem(storageKey) || defaultTheme
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement;
|
||||||
|
|
||||||
|
root.classList.remove("light", "dark");
|
||||||
|
|
||||||
|
if (theme === "system") {
|
||||||
|
const systemTheme = window.matchMedia(
|
||||||
|
"(prefers-color-scheme: dark)"
|
||||||
|
).matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
|
||||||
|
root.classList.add(systemTheme);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.classList.add(theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
theme,
|
||||||
|
setTheme: (theme: string) => {
|
||||||
|
localStorage.setItem(storageKey, theme);
|
||||||
|
setTheme(theme);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProviderContext.Provider {...props} value={value}>
|
||||||
|
{children}
|
||||||
|
</ThemeProviderContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = useContext(ThemeProviderContext);
|
||||||
|
|
||||||
|
if (context === undefined)
|
||||||
|
throw new Error("useTheme must be used within a ThemeProvider");
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
56
examples/pets/src/basicComponents/ui/button.tsx
Normal file
56
examples/pets/src/basicComponents/ui/button.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/basicComponents/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
25
examples/pets/src/basicComponents/ui/input.tsx
Normal file
25
examples/pets/src/basicComponents/ui/input.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/basicComponents/lib/utils"
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Input.displayName = "Input"
|
||||||
|
|
||||||
|
export { Input }
|
||||||
15
examples/pets/src/basicComponents/ui/skeleton.tsx
Normal file
15
examples/pets/src/basicComponents/ui/skeleton.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { cn } from "@/basicComponents/lib/utils"
|
||||||
|
|
||||||
|
function Skeleton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton }
|
||||||
127
examples/pets/src/basicComponents/ui/toast.tsx
Normal file
127
examples/pets/src/basicComponents/ui/toast.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/basicComponents/lib/utils"
|
||||||
|
|
||||||
|
const ToastProvider = ToastPrimitives.Provider
|
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||||
|
|
||||||
|
const toastVariants = cva(
|
||||||
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border bg-background text-foreground",
|
||||||
|
destructive:
|
||||||
|
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Toast = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||||
|
VariantProps<typeof toastVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<ToastPrimitives.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(toastVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
Toast.displayName = ToastPrimitives.Root.displayName
|
||||||
|
|
||||||
|
const ToastAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||||
|
|
||||||
|
const ToastClose = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Close
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
toast-close=""
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</ToastPrimitives.Close>
|
||||||
|
))
|
||||||
|
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm opacity-90", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||||
|
|
||||||
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||||
|
|
||||||
|
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ToastProps,
|
||||||
|
type ToastActionElement,
|
||||||
|
ToastProvider,
|
||||||
|
ToastViewport,
|
||||||
|
Toast,
|
||||||
|
ToastTitle,
|
||||||
|
ToastDescription,
|
||||||
|
ToastClose,
|
||||||
|
ToastAction,
|
||||||
|
}
|
||||||
33
examples/pets/src/basicComponents/ui/toaster.tsx
Normal file
33
examples/pets/src/basicComponents/ui/toaster.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
Toast,
|
||||||
|
ToastClose,
|
||||||
|
ToastDescription,
|
||||||
|
ToastProvider,
|
||||||
|
ToastTitle,
|
||||||
|
ToastViewport,
|
||||||
|
} from "@/basicComponents/ui/toast"
|
||||||
|
import { useToast } from "@/basicComponents/ui/use-toast"
|
||||||
|
|
||||||
|
export function Toaster() {
|
||||||
|
const { toasts } = useToast()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastProvider>
|
||||||
|
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||||
|
return (
|
||||||
|
<Toast key={id} {...props}>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
|
{description && (
|
||||||
|
<ToastDescription>{description}</ToastDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{action}
|
||||||
|
<ToastClose />
|
||||||
|
</Toast>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<ToastViewport />
|
||||||
|
</ToastProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
192
examples/pets/src/basicComponents/ui/use-toast.ts
Normal file
192
examples/pets/src/basicComponents/ui/use-toast.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// Inspired by react-hot-toast library
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ToastActionElement,
|
||||||
|
ToastProps,
|
||||||
|
} from "@/basicComponents/ui/toast"
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 1
|
||||||
|
const TOAST_REMOVE_DELAY = 1000000
|
||||||
|
|
||||||
|
type ToasterToast = ToastProps & {
|
||||||
|
id: string
|
||||||
|
title?: React.ReactNode
|
||||||
|
description?: React.ReactNode
|
||||||
|
action?: ToastActionElement
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionTypes = {
|
||||||
|
ADD_TOAST: "ADD_TOAST",
|
||||||
|
UPDATE_TOAST: "UPDATE_TOAST",
|
||||||
|
DISMISS_TOAST: "DISMISS_TOAST",
|
||||||
|
REMOVE_TOAST: "REMOVE_TOAST",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
count = (count + 1) % Number.MAX_VALUE
|
||||||
|
return count.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType = typeof actionTypes
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {
|
||||||
|
type: ActionType["ADD_TOAST"]
|
||||||
|
toast: ToasterToast
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["UPDATE_TOAST"]
|
||||||
|
toast: Partial<ToasterToast>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["DISMISS_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["REMOVE_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
toasts: ToasterToast[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
|
if (toastTimeouts.has(toastId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
toastTimeouts.delete(toastId)
|
||||||
|
dispatch({
|
||||||
|
type: "REMOVE_TOAST",
|
||||||
|
toastId: toastId,
|
||||||
|
})
|
||||||
|
}, TOAST_REMOVE_DELAY)
|
||||||
|
|
||||||
|
toastTimeouts.set(toastId, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducer = (state: State, action: Action): State => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "ADD_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
|
}
|
||||||
|
|
||||||
|
case "UPDATE_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
case "DISMISS_TOAST": {
|
||||||
|
const { toastId } = action
|
||||||
|
|
||||||
|
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||||
|
// but I'll keep it here for simplicity
|
||||||
|
if (toastId) {
|
||||||
|
addToRemoveQueue(toastId)
|
||||||
|
} else {
|
||||||
|
state.toasts.forEach((toast) => {
|
||||||
|
addToRemoveQueue(toast.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === toastId || toastId === undefined
|
||||||
|
? {
|
||||||
|
...t,
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
: t
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "REMOVE_TOAST":
|
||||||
|
if (action.toastId === undefined) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners: Array<(state: State) => void> = []
|
||||||
|
|
||||||
|
let memoryState: State = { toasts: [] }
|
||||||
|
|
||||||
|
function dispatch(action: Action) {
|
||||||
|
memoryState = reducer(memoryState, action)
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(memoryState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toast = Omit<ToasterToast, "id">
|
||||||
|
|
||||||
|
function toast({ ...props }: Toast) {
|
||||||
|
const id = genId()
|
||||||
|
|
||||||
|
const update = (props: ToasterToast) =>
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_TOAST",
|
||||||
|
toast: { ...props, id },
|
||||||
|
})
|
||||||
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "ADD_TOAST",
|
||||||
|
toast: {
|
||||||
|
...props,
|
||||||
|
id,
|
||||||
|
open: true,
|
||||||
|
onOpenChange: (open) => {
|
||||||
|
if (!open) dismiss()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
dismiss,
|
||||||
|
update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useToast() {
|
||||||
|
const [state, setState] = React.useState<State>(memoryState)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
listeners.push(setState)
|
||||||
|
return () => {
|
||||||
|
const index = listeners.indexOf(setState)
|
||||||
|
if (index > -1) {
|
||||||
|
listeners.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toast,
|
||||||
|
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useToast, toast }
|
||||||
48
examples/pets/src/components/Auth.tsx
Normal file
48
examples/pets/src/components/Auth.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { LocalAuthComponent } from "jazz-react-auth-local";
|
||||||
|
|
||||||
|
import { Input, Button } from "../basicComponents";
|
||||||
|
|
||||||
|
export const PrettyAuthUI: LocalAuthComponent = ({
|
||||||
|
loading,
|
||||||
|
logIn,
|
||||||
|
signUp,
|
||||||
|
}) => {
|
||||||
|
const [username, setUsername] = useState<string>("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center justify-center p-5">
|
||||||
|
{loading ? (
|
||||||
|
<div>Loading...</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-72 flex flex-col gap-4">
|
||||||
|
<form
|
||||||
|
className="w-72 flex flex-col gap-2"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
signUp(username);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="Display name"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
autoComplete="webauthn"
|
||||||
|
className="text-base"
|
||||||
|
/>
|
||||||
|
<Button asChild>
|
||||||
|
<Input
|
||||||
|
type="submit"
|
||||||
|
value="Sign Up as new account"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
<Button onClick={logIn}>
|
||||||
|
Log In with existing account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
46
examples/pets/src/components/NameBadge.tsx
Normal file
46
examples/pets/src/components/NameBadge.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { AccountID } from "cojson";
|
||||||
|
import { useProfile } from "jazz-react";
|
||||||
|
|
||||||
|
import { Skeleton } from "@/basicComponents";
|
||||||
|
import uniqolor from "uniqolor";
|
||||||
|
|
||||||
|
/** Walkthrough: Getting user profiles in `<NameBadge/>`
|
||||||
|
*
|
||||||
|
* `<NameBadge/>` uses `useProfile(accountID)`, which is a shorthand for
|
||||||
|
* useTelepathicState on an account's profile.
|
||||||
|
*
|
||||||
|
* Profiles are always a `CoMap<{name: string}>`, but they might have app-specific
|
||||||
|
* additional properties).
|
||||||
|
*
|
||||||
|
* In our case, we just display the profile name (which is set by the LocalAuth
|
||||||
|
* provider when we first create an account).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function NameBadge({ accountID }: { accountID?: AccountID }) {
|
||||||
|
const profile = useProfile(accountID);
|
||||||
|
|
||||||
|
return accountID && profile?.get("name") ? (
|
||||||
|
<span
|
||||||
|
className="rounded-full py-0.5 px-2 text-xs"
|
||||||
|
style={randomUserColor(accountID)}
|
||||||
|
>
|
||||||
|
{profile.get("name")}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<Skeleton className="mt-1 w-[50px] h-[1em] rounded-full" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomUserColor(accountID: AccountID) {
|
||||||
|
const theme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
|
||||||
|
const brightColor = uniqolor(accountID || "", { lightness: 80 }).color;
|
||||||
|
const darkColor = uniqolor(accountID || "", { lightness: 20 }).color;
|
||||||
|
|
||||||
|
return {
|
||||||
|
color: theme == "light" ? darkColor : brightColor,
|
||||||
|
background: theme == "light" ? brightColor : darkColor,
|
||||||
|
};
|
||||||
|
}
|
||||||
46
examples/pets/src/components/ShareButton.tsx
Normal file
46
examples/pets/src/components/ShareButton.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { PetPost } from "../1_types";
|
||||||
|
|
||||||
|
import { createInviteLink } from "jazz-react";
|
||||||
|
import QRCode from "qrcode";
|
||||||
|
|
||||||
|
import { useToast, Button } from "../basicComponents";
|
||||||
|
|
||||||
|
export function ShareButton({ petPost }: { petPost?: PetPost }) {
|
||||||
|
const [existingInviteLink, setExistingInviteLink] = useState<string>();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
return (
|
||||||
|
petPost?.group.myRole() === "admin" && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="py-0"
|
||||||
|
disabled={!petPost}
|
||||||
|
variant="outline"
|
||||||
|
onClick={async () => {
|
||||||
|
let inviteLink = existingInviteLink;
|
||||||
|
if (petPost && !inviteLink) {
|
||||||
|
inviteLink = createInviteLink(petPost, "writer");
|
||||||
|
setExistingInviteLink(inviteLink);
|
||||||
|
}
|
||||||
|
if (inviteLink) {
|
||||||
|
const qr = await QRCode.toDataURL(inviteLink, {
|
||||||
|
errorCorrectionLevel: "L",
|
||||||
|
});
|
||||||
|
navigator.clipboard.writeText(inviteLink).then(() =>
|
||||||
|
toast({
|
||||||
|
title: "Copied invite link to clipboard!",
|
||||||
|
description: (
|
||||||
|
<img src={qr} className="w-20 h-20" />
|
||||||
|
),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
76
examples/pets/src/index.css
Normal file
76
examples/pets/src/index.css
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--primary: 24 9.8% 10%;
|
||||||
|
--primary-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--secondary: 60 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--muted: 60 4.8% 95.9%;
|
||||||
|
--muted-foreground: 25 5.3% 44.7%;
|
||||||
|
|
||||||
|
--accent: 60 4.8% 95.9%;
|
||||||
|
--accent-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--border: 20 5.9% 90%;
|
||||||
|
--input: 20 5.9% 90%;
|
||||||
|
--ring: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 20 14.3% 4.1%;
|
||||||
|
--foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--card: 20 14.3% 4.1%;
|
||||||
|
--card-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--popover: 20 14.3% 4.1%;
|
||||||
|
--popover-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--primary: 60 9.1% 97.8%;
|
||||||
|
--primary-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--secondary: 12 6.5% 15.1%;
|
||||||
|
--secondary-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--muted: 12 6.5% 15.1%;
|
||||||
|
--muted-foreground: 24 5.4% 63.9%;
|
||||||
|
|
||||||
|
--accent: 12 6.5% 15.1%;
|
||||||
|
--accent-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--border: 12 6.5% 15.1%;
|
||||||
|
--input: 12 6.5% 15.1%;
|
||||||
|
--ring: 24 5.7% 82.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
examples/pets/src/router.ts
Normal file
37
examples/pets/src/router.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { CoID, LocalNode, CoValueImpl } from "cojson";
|
||||||
|
import { consumeInviteLinkFromWindowLocation } from "jazz-react";
|
||||||
|
|
||||||
|
export function useSimpleHashRouterThatAcceptsInvites<C extends CoValueImpl>(
|
||||||
|
localNode: LocalNode
|
||||||
|
) {
|
||||||
|
const [currentValueId, setCurrentValueId] = useState<CoID<C>>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = async () => {
|
||||||
|
const acceptedInvitation = await consumeInviteLinkFromWindowLocation<C>(localNode);
|
||||||
|
|
||||||
|
if (acceptedInvitation) {
|
||||||
|
setCurrentValueId(acceptedInvitation.valueID);
|
||||||
|
window.location.hash = acceptedInvitation.valueID;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentValueId(
|
||||||
|
(window.location.hash.slice(1) as CoID<C>) || undefined
|
||||||
|
);
|
||||||
|
};
|
||||||
|
window.addEventListener("hashchange", listener);
|
||||||
|
listener();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("hashchange", listener);
|
||||||
|
};
|
||||||
|
}, [localNode]);
|
||||||
|
|
||||||
|
const navigateToValue = useCallback((id: CoID<C> | undefined) => {
|
||||||
|
window.location.hash = id || "";
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [currentValueId, navigateToValue] as const;
|
||||||
|
}
|
||||||
1
examples/pets/src/vite-env.d.ts
vendored
Normal file
1
examples/pets/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
76
examples/pets/tailwind.config.js
Normal file
76
examples/pets/tailwind.config.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{ts,tsx}',
|
||||||
|
'./components/**/*.{ts,tsx}',
|
||||||
|
'./app/**/*.{ts,tsx}',
|
||||||
|
'./src/**/*.{ts,tsx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
}
|
||||||
29
examples/pets/tsconfig.json
Normal file
29
examples/pets/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
examples/pets/tsconfig.node.json
Normal file
10
examples/pets/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
16
examples/pets/vite.config.ts
Normal file
16
examples/pets/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
minify: false
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "jazz-example-todo",
|
"name": "jazz-example-todo",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.26",
|
"version": "0.0.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
"@types/qrcode": "^1.5.1",
|
"@types/qrcode": "^1.5.1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"jazz-react": "^0.1.12",
|
"jazz-react": "^0.2.2",
|
||||||
"jazz-react-auth-local": "^0.1.12",
|
"jazz-react-auth-local": "^0.2.2",
|
||||||
"lucide-react": "^0.274.0",
|
"lucide-react": "^0.274.0",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "0.1.11",
|
"version": "0.2.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.3",
|
"@types/jest": "^29.5.3",
|
||||||
"@types/ws": "^8.5.5",
|
"@types/ws": "^8.5.5",
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
"typescript": "5.0.2"
|
"typescript": "5.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cojson": "^0.1.10",
|
"cojson": "^0.2.1",
|
||||||
"cojson-storage-sqlite": "^0.1.8",
|
"cojson-storage-sqlite": "^0.2.1",
|
||||||
"ws": "^8.13.0"
|
"ws": "^8.13.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "cojson-storage-sqlite",
|
"name": "cojson-storage-sqlite",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.1.8",
|
"version": "0.2.1",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^8.5.2",
|
"better-sqlite3": "^8.5.2",
|
||||||
"cojson": "^0.1.10",
|
"cojson": "^0.2.1",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export class SQLiteStorage {
|
|||||||
done = result.done;
|
done = result.done;
|
||||||
|
|
||||||
if (result.value) {
|
if (result.value) {
|
||||||
this.handleSyncMessage(result.value);
|
await this.handleSyncMessage(result.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -217,7 +217,9 @@ export class SQLiteStorage {
|
|||||||
? Object.values(newContent.new).flatMap((sessionEntry) =>
|
? Object.values(newContent.new).flatMap((sessionEntry) =>
|
||||||
sessionEntry.newTransactions.flatMap((tx) => {
|
sessionEntry.newTransactions.flatMap((tx) => {
|
||||||
if (tx.privacy !== "trusting") return [];
|
if (tx.privacy !== "trusting") return [];
|
||||||
return tx.changes
|
// TODO: avoid parsing here?
|
||||||
|
return cojsonInternals
|
||||||
|
.parseJSON(tx.changes)
|
||||||
.map(
|
.map(
|
||||||
(change) =>
|
(change) =>
|
||||||
change &&
|
change &&
|
||||||
@@ -338,7 +340,7 @@ export class SQLiteStorage {
|
|||||||
lastSignature: msg.new[sessionID]!.lastSignature,
|
lastSignature: msg.new[sessionID]!.lastSignature,
|
||||||
};
|
};
|
||||||
|
|
||||||
const upsertedSession = (this.db
|
const upsertedSession = this.db
|
||||||
.prepare<[number, string, number, string]>(
|
.prepare<[number, string, number, string]>(
|
||||||
`INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature) VALUES (?, ?, ?, ?)
|
`INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature) VALUES (?, ?, ?, ?)
|
||||||
ON CONFLICT(coValue, sessionID) DO UPDATE SET lastIdx=excluded.lastIdx, lastSignature=excluded.lastSignature
|
ON CONFLICT(coValue, sessionID) DO UPDATE SET lastIdx=excluded.lastIdx, lastSignature=excluded.lastSignature
|
||||||
@@ -349,7 +351,7 @@ export class SQLiteStorage {
|
|||||||
sessionUpdate.sessionID,
|
sessionUpdate.sessionID,
|
||||||
sessionUpdate.lastIdx,
|
sessionUpdate.lastIdx,
|
||||||
sessionUpdate.lastSignature
|
sessionUpdate.lastSignature
|
||||||
) as {rowID: number});
|
) as { rowID: number };
|
||||||
|
|
||||||
const sessionRowID = upsertedSession.rowID;
|
const sessionRowID = upsertedSession.rowID;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "0.1.10",
|
"version": "0.2.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.3",
|
"@types/jest": "^29.5.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||||
@@ -19,9 +19,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/ciphers": "^0.1.3",
|
"@noble/ciphers": "^0.1.3",
|
||||||
"@noble/curves": "^1.1.0",
|
"@noble/curves": "^1.1.0",
|
||||||
"@noble/hashes": "^1.3.1",
|
|
||||||
"@scure/base": "^1.1.1",
|
"@scure/base": "^1.1.1",
|
||||||
"fast-json-stable-stringify": "https://github.com/tirithen/fast-json-stable-stringify#7a3dcf2",
|
"hash-wasm": "^4.9.0",
|
||||||
"isomorphic-streams": "https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
|
"isomorphic-streams": "https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { newRandomSessionID } from "./coValueCore.js";
|
import { newRandomSessionID } from "./coValueCore.js";
|
||||||
|
import { cojsonReady } from "./index.js";
|
||||||
import { LocalNode } from "./node.js";
|
import { LocalNode } from "./node.js";
|
||||||
import { connectedPeers } from "./streamUtils.js";
|
import { connectedPeers } from "./streamUtils.js";
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await cojsonReady;
|
||||||
|
});
|
||||||
|
|
||||||
test("Can create a node while creating a new account with profile", async () => {
|
test("Can create a node while creating a new account with profile", async () => {
|
||||||
const { node, accountID, accountSecret, sessionID } =
|
const { node, accountID, accountSecret, sessionID } =
|
||||||
LocalNode.withNewlyCreatedAccount("Hermes Puggington");
|
LocalNode.withNewlyCreatedAccount("Hermes Puggington");
|
||||||
|
|||||||
32
packages/cojson/src/base64url.test.ts
Normal file
32
packages/cojson/src/base64url.test.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { base64URLtoBytes, bytesToBase64url } from "./base64url";
|
||||||
|
|
||||||
|
const txt = new TextEncoder();
|
||||||
|
|
||||||
|
test("Test our Base64 URL encoding and decoding", () => {
|
||||||
|
// tests from the RFC
|
||||||
|
|
||||||
|
expect(base64URLtoBytes("")).toEqual(new Uint8Array([]));
|
||||||
|
expect(bytesToBase64url(new Uint8Array([]))).toEqual("");
|
||||||
|
|
||||||
|
expect(bytesToBase64url(txt.encode("f"))).toEqual("Zg==");
|
||||||
|
expect(bytesToBase64url(txt.encode("fo"))).toEqual("Zm8=");
|
||||||
|
expect(bytesToBase64url(txt.encode("foo"))).toEqual("Zm9v");
|
||||||
|
expect(bytesToBase64url(txt.encode("foob"))).toEqual("Zm9vYg==");
|
||||||
|
expect(bytesToBase64url(txt.encode("fooba"))).toEqual("Zm9vYmE=");
|
||||||
|
expect(bytesToBase64url(txt.encode("foobar"))).toEqual("Zm9vYmFy");
|
||||||
|
// reverse
|
||||||
|
expect(base64URLtoBytes("Zg==")).toEqual(txt.encode("f"));
|
||||||
|
expect(base64URLtoBytes("Zm8=")).toEqual(txt.encode("fo"));
|
||||||
|
expect(base64URLtoBytes("Zm9v")).toEqual(txt.encode("foo"));
|
||||||
|
expect(base64URLtoBytes("Zm9vYg==")).toEqual(txt.encode("foob"));
|
||||||
|
expect(base64URLtoBytes("Zm9vYmE=")).toEqual(txt.encode("fooba"));
|
||||||
|
expect(base64URLtoBytes("Zm9vYmFy")).toEqual(txt.encode("foobar"));
|
||||||
|
|
||||||
|
expect(base64URLtoBytes("V2hhdCBkb2VzIDIgKyAyLjEgZXF1YWw_PyB-IDQ=")).toEqual(
|
||||||
|
txt.encode("What does 2 + 2.1 equal?? ~ 4")
|
||||||
|
);
|
||||||
|
// reverse
|
||||||
|
expect(
|
||||||
|
bytesToBase64url(txt.encode("What does 2 + 2.1 equal?? ~ 4"))
|
||||||
|
).toEqual("V2hhdCBkb2VzIDIgKyAyLjEgZXF1YWw_PyB-IDQ=");
|
||||||
|
});
|
||||||
68
packages/cojson/src/base64url.ts
Normal file
68
packages/cojson/src/base64url.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
const encoder = new TextEncoder();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
export function base64URLtoBytes(base64: string) {
|
||||||
|
base64 = base64.replace(/=/g, "");
|
||||||
|
const n = base64.length;
|
||||||
|
const rem = n % 4;
|
||||||
|
const k = rem && rem - 1; // how many bytes the last base64 chunk encodes
|
||||||
|
const m = (n >> 2) * 3 + k; // total encoded bytes
|
||||||
|
|
||||||
|
const encoded = new Uint8Array(n + 3);
|
||||||
|
encoder.encodeInto(base64 + "===", encoded);
|
||||||
|
|
||||||
|
for (let i = 0, j = 0; i < n; i += 4, j += 3) {
|
||||||
|
const x =
|
||||||
|
(lookup[encoded[i]!]! << 18) +
|
||||||
|
(lookup[encoded[i + 1]!]! << 12) +
|
||||||
|
(lookup[encoded[i + 2]!]! << 6) +
|
||||||
|
lookup[encoded[i + 3]!]!;
|
||||||
|
encoded[j] = x >> 16;
|
||||||
|
encoded[j + 1] = (x >> 8) & 0xff;
|
||||||
|
encoded[j + 2] = x & 0xff;
|
||||||
|
}
|
||||||
|
return new Uint8Array(encoded.buffer, 0, m);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bytesToBase64url(bytes: Uint8Array) {
|
||||||
|
// const before = performance.now();
|
||||||
|
const m = bytes.length;
|
||||||
|
const k = m % 3;
|
||||||
|
const n = Math.floor(m / 3) * 4 + (k && k + 1);
|
||||||
|
const N = Math.ceil(m / 3) * 4;
|
||||||
|
const encoded = new Uint8Array(N);
|
||||||
|
|
||||||
|
for (let i = 0, j = 0; j < m; i += 4, j += 3) {
|
||||||
|
const y =
|
||||||
|
(bytes[j]! << 16) + (bytes[j + 1]! << 8) + (bytes[j + 2]! | 0);
|
||||||
|
encoded[i] = encodeLookup[y >> 18]!;
|
||||||
|
encoded[i + 1] = encodeLookup[(y >> 12) & 0x3f]!;
|
||||||
|
encoded[i + 2] = encodeLookup[(y >> 6) & 0x3f]!;
|
||||||
|
encoded[i + 3] = encodeLookup[y & 0x3f]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
let base64 = decoder.decode(new Uint8Array(encoded.buffer, 0, n));
|
||||||
|
if (k === 1) base64 += "==";
|
||||||
|
if (k === 2) base64 += "=";
|
||||||
|
// const after = performance.now();
|
||||||
|
// console.log(
|
||||||
|
// "bytesToBase64url bandwidth in MB/s for length",
|
||||||
|
// (1000 * bytes.length / (after - before)) / (1024 * 1024),
|
||||||
|
// bytes.length
|
||||||
|
// );
|
||||||
|
return base64;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alphabet =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||||
|
|
||||||
|
const lookup = new Uint8Array(128);
|
||||||
|
for (const [i, a] of Array.from(alphabet).entries()) {
|
||||||
|
lookup[a.charCodeAt(0)] = i;
|
||||||
|
}
|
||||||
|
lookup["=".charCodeAt(0)] = 0;
|
||||||
|
|
||||||
|
const encodeLookup = new Uint8Array(64);
|
||||||
|
for (const [i, a] of Array.from(alphabet).entries()) {
|
||||||
|
encodeLookup[i] = a.charCodeAt(0);
|
||||||
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import { accountOrAgentIDfromSessionID } from "./coValueCore.js";
|
import { accountOrAgentIDfromSessionID } from "./coValueCore.js";
|
||||||
import { BinaryCoStream } from "./coValues/coStream.js";
|
import { BinaryCoStream } from "./coValues/coStream.js";
|
||||||
import { createdNowUnique } from "./crypto.js";
|
import { createdNowUnique } from "./crypto.js";
|
||||||
|
import { MAX_RECOMMENDED_TX_SIZE, cojsonReady } from "./index.js";
|
||||||
import { LocalNode } from "./node.js";
|
import { LocalNode } from "./node.js";
|
||||||
import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
|
import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await cojsonReady;
|
||||||
|
});
|
||||||
|
|
||||||
test("Empty CoMap works", () => {
|
test("Empty CoMap works", () => {
|
||||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
||||||
|
|
||||||
@@ -377,11 +382,143 @@ test("Can push into BinaryCoStream", () => {
|
|||||||
|
|
||||||
content.edit((editable) => {
|
content.edit((editable) => {
|
||||||
editable.startBinaryStream({mimeType: "text/plain", fileName: "test.txt"}, "trusting");
|
editable.startBinaryStream({mimeType: "text/plain", fileName: "test.txt"}, "trusting");
|
||||||
expect(editable.getBinaryChunks()).toEqual({
|
expect(editable.getBinaryChunks(true)).toEqual({
|
||||||
mimeType: "text/plain",
|
mimeType: "text/plain",
|
||||||
fileName: "test.txt",
|
fileName: "test.txt",
|
||||||
chunks: [],
|
chunks: [],
|
||||||
finished: false,
|
finished: false,
|
||||||
});
|
});
|
||||||
|
editable.pushBinaryStreamChunk(new Uint8Array([1, 2, 3]), "trusting");
|
||||||
|
expect(editable.getBinaryChunks(true)).toEqual({
|
||||||
|
mimeType: "text/plain",
|
||||||
|
fileName: "test.txt",
|
||||||
|
chunks: [new Uint8Array([1, 2, 3])],
|
||||||
|
finished: false,
|
||||||
|
});
|
||||||
|
editable.pushBinaryStreamChunk(new Uint8Array([4, 5, 6]), "trusting");
|
||||||
|
|
||||||
|
expect(editable.getBinaryChunks(true)).toEqual({
|
||||||
|
mimeType: "text/plain",
|
||||||
|
fileName: "test.txt",
|
||||||
|
chunks: [new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6])],
|
||||||
|
finished: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
editable.endBinaryStream("trusting");
|
||||||
|
expect(editable.getBinaryChunks()).toEqual({
|
||||||
|
mimeType: "text/plain",
|
||||||
|
fileName: "test.txt",
|
||||||
|
chunks: [new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6])],
|
||||||
|
finished: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("When adding large transactions (small fraction of MAX_RECOMMENDED_TX_SIZE), we store an inbetween signature every time we reach MAX_RECOMMENDED_TX_SIZE and split up newContentSince accordingly", () => {
|
||||||
|
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
||||||
|
|
||||||
|
const coValue = node.createCoValue({
|
||||||
|
type: "costream",
|
||||||
|
ruleset: { type: "unsafeAllowAll" },
|
||||||
|
meta: { type: "binary" },
|
||||||
|
...createdNowUnique(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = coValue.getCurrentContent();
|
||||||
|
|
||||||
|
if (content.type !== "costream" || content.meta?.type !== "binary" || !(content instanceof BinaryCoStream)) {
|
||||||
|
throw new Error("Expected binary stream");
|
||||||
|
}
|
||||||
|
|
||||||
|
content.edit((editable) => {
|
||||||
|
editable.startBinaryStream({mimeType: "text/plain", fileName: "test.txt"}, "trusting");
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const chunk = new Uint8Array(MAX_RECOMMENDED_TX_SIZE/3 + 100);
|
||||||
|
|
||||||
|
content.edit((editable) => {
|
||||||
|
editable.pushBinaryStreamChunk(chunk, "trusting");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
content.edit((editable) => {
|
||||||
|
editable.endBinaryStream("trusting");
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionEntry = coValue._sessions[node.currentSessionID]!;
|
||||||
|
expect(sessionEntry.transactions.length).toEqual(12);
|
||||||
|
expect(sessionEntry.signatureAfter[0]).not.toBeDefined();
|
||||||
|
expect(sessionEntry.signatureAfter[1]).not.toBeDefined();
|
||||||
|
expect(sessionEntry.signatureAfter[2]).not.toBeDefined();
|
||||||
|
expect(sessionEntry.signatureAfter[3]).toBeDefined();
|
||||||
|
expect(sessionEntry.signatureAfter[4]).not.toBeDefined();
|
||||||
|
expect(sessionEntry.signatureAfter[5]).not.toBeDefined();
|
||||||
|
expect(sessionEntry.signatureAfter[6]).toBeDefined();
|
||||||
|
expect(sessionEntry.signatureAfter[7]).not.toBeDefined();
|
||||||
|
expect(sessionEntry.signatureAfter[8]).not.toBeDefined();
|
||||||
|
expect(sessionEntry.signatureAfter[9]).toBeDefined();
|
||||||
|
expect(sessionEntry.signatureAfter[10]).not.toBeDefined();
|
||||||
|
expect(sessionEntry.signatureAfter[11]).not.toBeDefined();
|
||||||
|
|
||||||
|
const newContent = coValue.newContentSince({id: coValue.id, header: false, sessions: {}})!;
|
||||||
|
|
||||||
|
expect(newContent.length).toEqual(5)
|
||||||
|
expect(newContent[0]!.header).toBeDefined();
|
||||||
|
expect(newContent[1]!.new[node.currentSessionID]!.lastSignature).toEqual(sessionEntry.signatureAfter[3]);
|
||||||
|
expect(newContent[2]!.new[node.currentSessionID]!.lastSignature).toEqual(sessionEntry.signatureAfter[6]);
|
||||||
|
expect(newContent[3]!.new[node.currentSessionID]!.lastSignature).toEqual(sessionEntry.signatureAfter[9]);
|
||||||
|
expect(newContent[4]!.new[node.currentSessionID]!.lastSignature).toEqual(sessionEntry.lastSignature);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("When adding large transactions (bigger than MAX_RECOMMENDED_TX_SIZE), we store an inbetween signature after every large transaction and split up newContentSince accordingly", () => {
|
||||||
|
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
||||||
|
|
||||||
|
const coValue = node.createCoValue({
|
||||||
|
type: "costream",
|
||||||
|
ruleset: { type: "unsafeAllowAll" },
|
||||||
|
meta: { type: "binary" },
|
||||||
|
...createdNowUnique(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = coValue.getCurrentContent();
|
||||||
|
|
||||||
|
if (content.type !== "costream" || content.meta?.type !== "binary" || !(content instanceof BinaryCoStream)) {
|
||||||
|
throw new Error("Expected binary stream");
|
||||||
|
}
|
||||||
|
|
||||||
|
content.edit((editable) => {
|
||||||
|
editable.startBinaryStream({mimeType: "text/plain", fileName: "test.txt"}, "trusting");
|
||||||
|
});
|
||||||
|
|
||||||
|
const chunk = new Uint8Array(MAX_RECOMMENDED_TX_SIZE + 100);
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
content.edit((editable) => {
|
||||||
|
editable.pushBinaryStreamChunk(chunk, "trusting");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
content.edit((editable) => {
|
||||||
|
editable.endBinaryStream("trusting");
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionEntry = coValue._sessions[node.currentSessionID]!;
|
||||||
|
expect(sessionEntry.transactions.length).toEqual(5);
|
||||||
|
expect(sessionEntry.signatureAfter[0]).not.toBeDefined();
|
||||||
|
expect(sessionEntry.signatureAfter[1]).toBeDefined();
|
||||||
|
expect(sessionEntry.signatureAfter[2]).toBeDefined();
|
||||||
|
expect(sessionEntry.signatureAfter[3]).toBeDefined();
|
||||||
|
expect(sessionEntry.signatureAfter[4]).not.toBeDefined();
|
||||||
|
|
||||||
|
const newContent = coValue.newContentSince({id: coValue.id, header: false, sessions: {}})!;
|
||||||
|
|
||||||
|
expect(newContent.length).toEqual(5)
|
||||||
|
expect(newContent[0]!.header).toBeDefined();
|
||||||
|
expect(newContent[1]!.new[node.currentSessionID]!.lastSignature).toEqual(sessionEntry.signatureAfter[1]);
|
||||||
|
expect(newContent[2]!.new[node.currentSessionID]!.lastSignature).toEqual(sessionEntry.signatureAfter[2]);
|
||||||
|
expect(newContent[3]!.new[node.currentSessionID]!.lastSignature).toEqual(sessionEntry.signatureAfter[3]);
|
||||||
|
expect(newContent[4]!.new[node.currentSessionID]!.lastSignature).toEqual(sessionEntry.lastSignature);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export interface CoValue {
|
|||||||
export interface WriteableCoValue extends CoValue {}
|
export interface WriteableCoValue extends CoValue {}
|
||||||
|
|
||||||
export type CoValueImpl =
|
export type CoValueImpl =
|
||||||
| CoMap<{ [key: string]: JsonValue }, JsonObject | null>
|
| CoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>
|
||||||
| CoList<JsonValue, JsonObject | null>
|
| CoList<JsonValue, JsonObject | null>
|
||||||
| CoStream<JsonValue, JsonObject | null>
|
| CoStream<JsonValue, JsonObject | null>
|
||||||
| BinaryCoStream<BinaryCoStreamMeta>
|
| BinaryCoStream<BinaryCoStreamMeta>
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ import { createdNowUnique, getAgentSignerSecret, newRandomAgentSecret, sign } fr
|
|||||||
import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
|
import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
|
||||||
import { MapOpPayload } from "./coValues/coMap.js";
|
import { MapOpPayload } from "./coValues/coMap.js";
|
||||||
import { Role } from "./permissions.js";
|
import { Role } from "./permissions.js";
|
||||||
|
import { cojsonReady } from "./index.js";
|
||||||
|
import { stableStringify } from "./jsonStringify.js";
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await cojsonReady;
|
||||||
|
});
|
||||||
|
|
||||||
test("Can create coValue with new agent credentials and add transaction to it", () => {
|
test("Can create coValue with new agent credentials and add transaction to it", () => {
|
||||||
const [account, sessionID] = randomAnonymousAccountAndSessionID();
|
const [account, sessionID] = randomAnonymousAccountAndSessionID();
|
||||||
@@ -19,11 +25,11 @@ test("Can create coValue with new agent credentials and add transaction to it",
|
|||||||
const transaction: Transaction = {
|
const transaction: Transaction = {
|
||||||
privacy: "trusting",
|
privacy: "trusting",
|
||||||
madeAt: Date.now(),
|
madeAt: Date.now(),
|
||||||
changes: [
|
changes: stableStringify([
|
||||||
{
|
{
|
||||||
hello: "world",
|
hello: "world",
|
||||||
},
|
},
|
||||||
],
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { expectedNewHash } = coValue.expectedNewHashAfter(
|
const { expectedNewHash } = coValue.expectedNewHashAfter(
|
||||||
@@ -56,11 +62,11 @@ test("transactions with wrong signature are rejected", () => {
|
|||||||
const transaction: Transaction = {
|
const transaction: Transaction = {
|
||||||
privacy: "trusting",
|
privacy: "trusting",
|
||||||
madeAt: Date.now(),
|
madeAt: Date.now(),
|
||||||
changes: [
|
changes: stableStringify([
|
||||||
{
|
{
|
||||||
hello: "world",
|
hello: "world",
|
||||||
},
|
},
|
||||||
],
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { expectedNewHash } = coValue.expectedNewHashAfter(
|
const { expectedNewHash } = coValue.expectedNewHashAfter(
|
||||||
@@ -92,11 +98,11 @@ test("transactions with correctly signed, but wrong hash are rejected", () => {
|
|||||||
const transaction: Transaction = {
|
const transaction: Transaction = {
|
||||||
privacy: "trusting",
|
privacy: "trusting",
|
||||||
madeAt: Date.now(),
|
madeAt: Date.now(),
|
||||||
changes: [
|
changes: stableStringify([
|
||||||
{
|
{
|
||||||
hello: "world",
|
hello: "world",
|
||||||
},
|
},
|
||||||
],
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { expectedNewHash } = coValue.expectedNewHashAfter(
|
const { expectedNewHash } = coValue.expectedNewHashAfter(
|
||||||
@@ -105,11 +111,11 @@ test("transactions with correctly signed, but wrong hash are rejected", () => {
|
|||||||
{
|
{
|
||||||
privacy: "trusting",
|
privacy: "trusting",
|
||||||
madeAt: Date.now(),
|
madeAt: Date.now(),
|
||||||
changes: [
|
changes: stableStringify([
|
||||||
{
|
{
|
||||||
hello: "wrong",
|
hello: "wrong",
|
||||||
},
|
},
|
||||||
],
|
]),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -149,13 +155,13 @@ test("New transactions in a group correctly update owned values, including subsc
|
|||||||
const resignationThatWeJustLearnedAbout = {
|
const resignationThatWeJustLearnedAbout = {
|
||||||
privacy: "trusting",
|
privacy: "trusting",
|
||||||
madeAt: timeBeforeEdit,
|
madeAt: timeBeforeEdit,
|
||||||
changes: [
|
changes: stableStringify([
|
||||||
{
|
{
|
||||||
op: "set",
|
op: "set",
|
||||||
key: account.id,
|
key: account.id,
|
||||||
value: "revoked"
|
value: "revoked"
|
||||||
} satisfies MapOpPayload<typeof account.id, Role>
|
} satisfies MapOpPayload<typeof account.id, Role>
|
||||||
]
|
])
|
||||||
} satisfies Transaction;
|
} satisfies Transaction;
|
||||||
|
|
||||||
const { expectedNewHash } = group.underlyingMap.core.expectedNewHashAfter(sessionID, [
|
const { expectedNewHash } = group.underlyingMap.core.expectedNewHashAfter(sessionID, [
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import {
|
|||||||
sign,
|
sign,
|
||||||
verify,
|
verify,
|
||||||
encryptForTransaction,
|
encryptForTransaction,
|
||||||
decryptForTransaction,
|
|
||||||
KeyID,
|
KeyID,
|
||||||
decryptKeySecret,
|
decryptKeySecret,
|
||||||
getAgentSignerID,
|
getAgentSignerID,
|
||||||
getAgentSealerID,
|
getAgentSealerID,
|
||||||
|
decryptRawForTransaction,
|
||||||
} from "./crypto.js";
|
} from "./crypto.js";
|
||||||
import { JsonObject, JsonValue } from "./jsonValue.js";
|
import { JsonObject, JsonValue } from "./jsonValue.js";
|
||||||
import { base58 } from "@scure/base";
|
import { base58 } from "@scure/base";
|
||||||
@@ -32,10 +32,10 @@ import { LocalNode } from "./node.js";
|
|||||||
import { CoValueKnownState, NewContentMessage } from "./sync.js";
|
import { CoValueKnownState, NewContentMessage } from "./sync.js";
|
||||||
import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
|
import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
|
||||||
import { CoList } from "./coValues/coList.js";
|
import { CoList } from "./coValues/coList.js";
|
||||||
import {
|
import { AccountID, GeneralizedControlledAccount } from "./account.js";
|
||||||
AccountID,
|
import { Stringified, stableStringify } from "./jsonStringify.js";
|
||||||
GeneralizedControlledAccount,
|
|
||||||
} from "./account.js";
|
export const MAX_RECOMMENDED_TX_SIZE = 100 * 1024;
|
||||||
|
|
||||||
export type CoValueHeader = {
|
export type CoValueHeader = {
|
||||||
type: CoValueImpl["type"];
|
type: CoValueImpl["type"];
|
||||||
@@ -64,6 +64,7 @@ type SessionLog = {
|
|||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
lastHash?: Hash;
|
lastHash?: Hash;
|
||||||
streamingHash: StreamingHash;
|
streamingHash: StreamingHash;
|
||||||
|
signatureAfter: { [txIdx: number]: Signature | undefined };
|
||||||
lastSignature: Signature;
|
lastSignature: Signature;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,14 +81,14 @@ export type PrivateTransaction = {
|
|||||||
export type TrustingTransaction = {
|
export type TrustingTransaction = {
|
||||||
privacy: "trusting";
|
privacy: "trusting";
|
||||||
madeAt: number;
|
madeAt: number;
|
||||||
changes: JsonValue[];
|
changes: Stringified<JsonValue[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Transaction = PrivateTransaction | TrustingTransaction;
|
export type Transaction = PrivateTransaction | TrustingTransaction;
|
||||||
|
|
||||||
export type DecryptedTransaction = {
|
export type DecryptedTransaction = {
|
||||||
txID: TransactionID;
|
txID: TransactionID;
|
||||||
changes: JsonValue[];
|
changes: Stringified<JsonValue[]>;
|
||||||
madeAt: number;
|
madeAt: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,6 +101,11 @@ export class CoValueCore {
|
|||||||
_sessions: { [key: SessionID]: SessionLog };
|
_sessions: { [key: SessionID]: SessionLog };
|
||||||
_cachedContent?: CoValueImpl;
|
_cachedContent?: CoValueImpl;
|
||||||
listeners: Set<(content?: CoValueImpl) => void> = new Set();
|
listeners: Set<(content?: CoValueImpl) => void> = new Set();
|
||||||
|
_decryptionCache: {
|
||||||
|
[key: Encrypted<JsonValue[], JsonValue>]:
|
||||||
|
| Stringified<JsonValue[]>
|
||||||
|
| undefined;
|
||||||
|
} = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
header: CoValueHeader,
|
header: CoValueHeader,
|
||||||
@@ -186,10 +192,16 @@ export class CoValueCore {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// const beforeHash = performance.now();
|
||||||
const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
|
const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
|
||||||
sessionID,
|
sessionID,
|
||||||
newTransactions
|
newTransactions
|
||||||
);
|
);
|
||||||
|
// const afterHash = performance.now();
|
||||||
|
// console.log(
|
||||||
|
// "Hashing took",
|
||||||
|
// afterHash - beforeHash
|
||||||
|
// );
|
||||||
|
|
||||||
if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
|
if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
|
||||||
console.warn("Invalid hash", {
|
console.warn("Invalid hash", {
|
||||||
@@ -199,36 +211,169 @@ export class CoValueCore {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// const beforeVerify = performance.now();
|
||||||
if (!verify(newSignature, expectedNewHash, signerID)) {
|
if (!verify(newSignature, expectedNewHash, signerID)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Invalid signature",
|
"Invalid signature in",
|
||||||
|
this.id,
|
||||||
newSignature,
|
newSignature,
|
||||||
expectedNewHash,
|
expectedNewHash,
|
||||||
signerID
|
signerID
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// const afterVerify = performance.now();
|
||||||
|
// console.log(
|
||||||
|
// "Verify took",
|
||||||
|
// afterVerify - beforeVerify
|
||||||
|
// );
|
||||||
|
|
||||||
|
this.doAddTransactions(
|
||||||
|
sessionID,
|
||||||
|
newTransactions,
|
||||||
|
newSignature,
|
||||||
|
expectedNewHash,
|
||||||
|
newStreamingHash
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tryAddTransactionsAsync(
|
||||||
|
sessionID: SessionID,
|
||||||
|
newTransactions: Transaction[],
|
||||||
|
givenExpectedNewHash: Hash | undefined,
|
||||||
|
newSignature: Signature
|
||||||
|
): Promise<boolean> {
|
||||||
|
const signerID = getAgentSignerID(
|
||||||
|
this.node.resolveAccountAgent(
|
||||||
|
accountOrAgentIDfromSessionID(sessionID),
|
||||||
|
"Expected to know signer of transaction"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!signerID) {
|
||||||
|
console.warn(
|
||||||
|
"Unknown agent",
|
||||||
|
accountOrAgentIDfromSessionID(sessionID)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nTxBefore = this.sessions[sessionID]?.transactions.length ?? 0;
|
||||||
|
|
||||||
|
// const beforeHash = performance.now();
|
||||||
|
const { expectedNewHash, newStreamingHash } =
|
||||||
|
await this.expectedNewHashAfterAsync(sessionID, newTransactions);
|
||||||
|
// const afterHash = performance.now();
|
||||||
|
// console.log(
|
||||||
|
// "Hashing took",
|
||||||
|
// afterHash - beforeHash
|
||||||
|
// );
|
||||||
|
|
||||||
|
const nTxAfter = this.sessions[sessionID]?.transactions.length ?? 0;
|
||||||
|
|
||||||
|
if (nTxAfter !== nTxBefore) {
|
||||||
|
const newTransactionLengthBefore = newTransactions.length;
|
||||||
|
newTransactions = newTransactions.slice(nTxAfter - nTxBefore);
|
||||||
|
console.warn("Transactions changed while async hashing", {
|
||||||
|
nTxBefore,
|
||||||
|
nTxAfter,
|
||||||
|
newTransactionLengthBefore,
|
||||||
|
remainingNewTransactions: newTransactions.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
|
||||||
|
console.warn("Invalid hash", {
|
||||||
|
expectedNewHash,
|
||||||
|
givenExpectedNewHash,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// const beforeVerify = performance.now();
|
||||||
|
if (!verify(newSignature, expectedNewHash, signerID)) {
|
||||||
|
console.warn(
|
||||||
|
"Invalid signature in",
|
||||||
|
this.id,
|
||||||
|
newSignature,
|
||||||
|
expectedNewHash,
|
||||||
|
signerID
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// const afterVerify = performance.now();
|
||||||
|
// console.log(
|
||||||
|
// "Verify took",
|
||||||
|
// afterVerify - beforeVerify
|
||||||
|
// );
|
||||||
|
|
||||||
|
this.doAddTransactions(
|
||||||
|
sessionID,
|
||||||
|
newTransactions,
|
||||||
|
newSignature,
|
||||||
|
expectedNewHash,
|
||||||
|
newStreamingHash
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private doAddTransactions(
|
||||||
|
sessionID: SessionID,
|
||||||
|
newTransactions: Transaction[],
|
||||||
|
newSignature: Signature,
|
||||||
|
expectedNewHash: Hash,
|
||||||
|
newStreamingHash: StreamingHash
|
||||||
|
) {
|
||||||
const transactions = this.sessions[sessionID]?.transactions ?? [];
|
const transactions = this.sessions[sessionID]?.transactions ?? [];
|
||||||
|
|
||||||
transactions.push(...newTransactions);
|
transactions.push(...newTransactions);
|
||||||
|
|
||||||
|
const signatureAfter = this.sessions[sessionID]?.signatureAfter ?? {};
|
||||||
|
|
||||||
|
const lastInbetweenSignatureIdx = Object.keys(signatureAfter).reduce(
|
||||||
|
(max, idx) => (parseInt(idx) > max ? parseInt(idx) : max),
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
|
||||||
|
const sizeOfTxsSinceLastInbetweenSignature = transactions
|
||||||
|
.slice(lastInbetweenSignatureIdx + 1)
|
||||||
|
.reduce(
|
||||||
|
(sum, tx) =>
|
||||||
|
sum +
|
||||||
|
(tx.privacy === "private"
|
||||||
|
? tx.encryptedChanges.length
|
||||||
|
: tx.changes.length),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sizeOfTxsSinceLastInbetweenSignature > 100 * 1024) {
|
||||||
|
// console.log(
|
||||||
|
// "Saving inbetween signature for tx ",
|
||||||
|
// sessionID,
|
||||||
|
// transactions.length - 1,
|
||||||
|
// sizeOfTxsSinceLastInbetweenSignature
|
||||||
|
// );
|
||||||
|
signatureAfter[transactions.length - 1] = newSignature;
|
||||||
|
}
|
||||||
|
|
||||||
this._sessions[sessionID] = {
|
this._sessions[sessionID] = {
|
||||||
transactions,
|
transactions,
|
||||||
lastHash: expectedNewHash,
|
lastHash: expectedNewHash,
|
||||||
streamingHash: newStreamingHash,
|
streamingHash: newStreamingHash,
|
||||||
lastSignature: newSignature,
|
lastSignature: newSignature,
|
||||||
|
signatureAfter: signatureAfter,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._cachedContent = undefined;
|
this._cachedContent = undefined;
|
||||||
|
|
||||||
const content = this.getCurrentContent();
|
if (this.listeners.size > 0) {
|
||||||
|
const content = this.getCurrentContent();
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
listener(content);
|
listener(content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(listener: (content?: CoValueImpl) => void): () => void {
|
subscribe(listener: (content?: CoValueImpl) => void): () => void {
|
||||||
@@ -259,6 +404,32 @@ export class CoValueCore {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async expectedNewHashAfterAsync(
|
||||||
|
sessionID: SessionID,
|
||||||
|
newTransactions: Transaction[]
|
||||||
|
): Promise<{ expectedNewHash: Hash; newStreamingHash: StreamingHash }> {
|
||||||
|
const streamingHash =
|
||||||
|
this.sessions[sessionID]?.streamingHash.clone() ??
|
||||||
|
new StreamingHash();
|
||||||
|
let before = performance.now();
|
||||||
|
for (const transaction of newTransactions) {
|
||||||
|
streamingHash.update(transaction);
|
||||||
|
const after = performance.now();
|
||||||
|
if (after - before > 1) {
|
||||||
|
// console.log("Hashing blocked for", after - before);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
before = performance.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStreamingHash = streamingHash.clone();
|
||||||
|
|
||||||
|
return {
|
||||||
|
expectedNewHash: streamingHash.digest(),
|
||||||
|
newStreamingHash,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
makeTransaction(
|
makeTransaction(
|
||||||
changes: JsonValue[],
|
changes: JsonValue[],
|
||||||
privacy: "private" | "trusting"
|
privacy: "private" | "trusting"
|
||||||
@@ -276,20 +447,24 @@ export class CoValueCore {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const encrypted = encryptForTransaction(changes, keySecret, {
|
||||||
|
in: this.id,
|
||||||
|
tx: this.nextTransactionID(),
|
||||||
|
});
|
||||||
|
|
||||||
|
this._decryptionCache[encrypted] = stableStringify(changes);
|
||||||
|
|
||||||
transaction = {
|
transaction = {
|
||||||
privacy: "private",
|
privacy: "private",
|
||||||
madeAt,
|
madeAt,
|
||||||
keyUsed: keyID,
|
keyUsed: keyID,
|
||||||
encryptedChanges: encryptForTransaction(changes, keySecret, {
|
encryptedChanges: encrypted,
|
||||||
in: this.id,
|
|
||||||
tx: this.nextTransactionID(),
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
transaction = {
|
transaction = {
|
||||||
privacy: "trusting",
|
privacy: "trusting",
|
||||||
madeAt,
|
madeAt,
|
||||||
changes,
|
changes: stableStringify(changes),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,14 +534,21 @@ export class CoValueCore {
|
|||||||
if (!readKey) {
|
if (!readKey) {
|
||||||
return undefined;
|
return undefined;
|
||||||
} else {
|
} else {
|
||||||
const decrytedChanges = decryptForTransaction(
|
let decrytedChanges =
|
||||||
tx.encryptedChanges,
|
this._decryptionCache[tx.encryptedChanges];
|
||||||
readKey,
|
|
||||||
{
|
if (!decrytedChanges) {
|
||||||
in: this.id,
|
decrytedChanges = decryptRawForTransaction(
|
||||||
tx: txID,
|
tx.encryptedChanges,
|
||||||
}
|
readKey,
|
||||||
);
|
{
|
||||||
|
in: this.id,
|
||||||
|
tx: txID,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this._decryptionCache[tx.encryptedChanges] =
|
||||||
|
decrytedChanges;
|
||||||
|
}
|
||||||
|
|
||||||
if (!decrytedChanges) {
|
if (!decrytedChanges) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -537,47 +719,95 @@ export class CoValueCore {
|
|||||||
|
|
||||||
newContentSince(
|
newContentSince(
|
||||||
knownState: CoValueKnownState | undefined
|
knownState: CoValueKnownState | undefined
|
||||||
): NewContentMessage | undefined {
|
): NewContentMessage[] | undefined {
|
||||||
const newContent: NewContentMessage = {
|
let currentPiece: NewContentMessage = {
|
||||||
action: "content",
|
action: "content",
|
||||||
id: this.id,
|
id: this.id,
|
||||||
header: knownState?.header ? undefined : this.header,
|
header: knownState?.header ? undefined : this.header,
|
||||||
new: Object.fromEntries(
|
new: {},
|
||||||
Object.entries(this.sessions)
|
|
||||||
.map(([sessionID, log]) => {
|
|
||||||
const newTransactions = log.transactions.slice(
|
|
||||||
knownState?.sessions[sessionID as SessionID] || 0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
newTransactions.length === 0 ||
|
|
||||||
!log.lastHash ||
|
|
||||||
!log.lastSignature
|
|
||||||
) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
sessionID,
|
|
||||||
{
|
|
||||||
after:
|
|
||||||
knownState?.sessions[
|
|
||||||
sessionID as SessionID
|
|
||||||
] || 0,
|
|
||||||
newTransactions,
|
|
||||||
lastSignature: log.lastSignature,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
})
|
|
||||||
.filter((x): x is Exclude<typeof x, undefined> => !!x)
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!newContent.header && Object.keys(newContent.new).length === 0) {
|
const pieces = [currentPiece];
|
||||||
|
|
||||||
|
const sentState: CoValueKnownState["sessions"] = {
|
||||||
|
...knownState?.sessions,
|
||||||
|
};
|
||||||
|
|
||||||
|
let newTxsWereAdded = true;
|
||||||
|
let pieceSize = 0;
|
||||||
|
while (newTxsWereAdded) {
|
||||||
|
newTxsWereAdded = false;
|
||||||
|
|
||||||
|
for (const [sessionID, log] of Object.entries(this.sessions) as [
|
||||||
|
SessionID,
|
||||||
|
SessionLog
|
||||||
|
][]) {
|
||||||
|
const nextKnownSignatureIdx = Object.keys(log.signatureAfter)
|
||||||
|
.map(Number)
|
||||||
|
.sort((a, b) => a - b)
|
||||||
|
.find((idx) => idx >= (sentState[sessionID] ?? -1));
|
||||||
|
|
||||||
|
const txsToAdd = log.transactions.slice(
|
||||||
|
sentState[sessionID] ?? 0,
|
||||||
|
nextKnownSignatureIdx === undefined
|
||||||
|
? undefined
|
||||||
|
: nextKnownSignatureIdx + 1
|
||||||
|
);
|
||||||
|
|
||||||
|
if (txsToAdd.length === 0) continue;
|
||||||
|
|
||||||
|
newTxsWereAdded = true;
|
||||||
|
|
||||||
|
const oldPieceSize = pieceSize;
|
||||||
|
pieceSize += txsToAdd.reduce(
|
||||||
|
(sum, tx) =>
|
||||||
|
sum +
|
||||||
|
(tx.privacy === "private"
|
||||||
|
? tx.encryptedChanges.length
|
||||||
|
: tx.changes.length),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pieceSize >= MAX_RECOMMENDED_TX_SIZE) {
|
||||||
|
currentPiece = {
|
||||||
|
action: "content",
|
||||||
|
id: this.id,
|
||||||
|
header: undefined,
|
||||||
|
new: {},
|
||||||
|
};
|
||||||
|
pieces.push(currentPiece);
|
||||||
|
pieceSize = pieceSize - oldPieceSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sessionEntry = currentPiece.new[sessionID];
|
||||||
|
if (!sessionEntry) {
|
||||||
|
sessionEntry = {
|
||||||
|
after: sentState[sessionID] ?? 0,
|
||||||
|
newTransactions: [],
|
||||||
|
lastSignature: "WILL_BE_REPLACED" as Signature
|
||||||
|
};
|
||||||
|
currentPiece.new[sessionID] = sessionEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionEntry.newTransactions.push(...txsToAdd);
|
||||||
|
sessionEntry.lastSignature = nextKnownSignatureIdx === undefined
|
||||||
|
? log.lastSignature!
|
||||||
|
: log.signatureAfter[nextKnownSignatureIdx]!
|
||||||
|
|
||||||
|
sentState[sessionID] =
|
||||||
|
(sentState[sessionID] || 0) + txsToAdd.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const piecesWithContent = pieces.filter(
|
||||||
|
(piece) => Object.keys(piece.new).length > 0 || piece.header
|
||||||
|
);
|
||||||
|
|
||||||
|
if (piecesWithContent.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return newContent;
|
return piecesWithContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDependedOnCoValues(): RawCoID[] {
|
getDependedOnCoValues(): RawCoID[] {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CoValueCore, accountOrAgentIDfromSessionID } from "../coValueCore.js";
|
|||||||
import { SessionID, TransactionID } from "../ids.js";
|
import { SessionID, TransactionID } from "../ids.js";
|
||||||
import { Group } from "../group.js";
|
import { Group } from "../group.js";
|
||||||
import { AccountID, isAccountID } from "../account.js";
|
import { AccountID, isAccountID } from "../account.js";
|
||||||
|
import { parseJSON } from "../jsonStringify.js";
|
||||||
|
|
||||||
type OpID = TransactionID & { changeIdx: number };
|
type OpID = TransactionID & { changeIdx: number };
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ export class CoList<T extends JsonValue, Meta extends JsonObject | null = null>
|
|||||||
changes,
|
changes,
|
||||||
madeAt,
|
madeAt,
|
||||||
} of this.core.getValidSortedTransactions()) {
|
} of this.core.getValidSortedTransactions()) {
|
||||||
for (const [changeIdx, changeUntyped] of changes.entries()) {
|
for (const [changeIdx, changeUntyped] of parseJSON(changes).entries()) {
|
||||||
const change = changeUntyped as ListOpPayload<T>;
|
const change = changeUntyped as ListOpPayload<T>;
|
||||||
|
|
||||||
if (change.op === "pre" || change.op === "app") {
|
if (change.op === "pre" || change.op === "app") {
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import { CoID, ReadableCoValue, WriteableCoValue } from '../coValue.js';
|
|||||||
import { CoValueCore, accountOrAgentIDfromSessionID } from '../coValueCore.js';
|
import { CoValueCore, accountOrAgentIDfromSessionID } from '../coValueCore.js';
|
||||||
import { AccountID, isAccountID } from '../account.js';
|
import { AccountID, isAccountID } from '../account.js';
|
||||||
import { Group } from '../group.js';
|
import { Group } from '../group.js';
|
||||||
|
import { parseJSON } from '../jsonStringify.js';
|
||||||
|
|
||||||
type MapOp<K extends string, V extends JsonValue> = {
|
type MapOp<K extends string, V extends JsonValue | undefined> = {
|
||||||
txID: TransactionID;
|
txID: TransactionID;
|
||||||
madeAt: number;
|
madeAt: number;
|
||||||
changeIdx: number;
|
changeIdx: number;
|
||||||
} & MapOpPayload<K, V>;
|
} & MapOpPayload<K, V>;
|
||||||
// TODO: add after TransactionID[] for conflicts/ordering
|
// TODO: add after TransactionID[] for conflicts/ordering
|
||||||
|
|
||||||
export type MapOpPayload<K extends string, V extends JsonValue> = {
|
export type MapOpPayload<K extends string, V extends JsonValue | undefined> = {
|
||||||
op: "set";
|
op: "set";
|
||||||
key: K;
|
key: K;
|
||||||
value: V;
|
value: V;
|
||||||
@@ -22,18 +23,16 @@ export type MapOpPayload<K extends string, V extends JsonValue> = {
|
|||||||
key: K;
|
key: K;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MapK<M extends { [key: string]: JsonValue; }> = keyof M & string;
|
export type MapK<M extends { [key: string]: JsonValue | undefined; }> = keyof M & string;
|
||||||
export type MapV<M extends { [key: string]: JsonValue; }> = M[MapK<M>];
|
export type MapV<M extends { [key: string]: JsonValue | undefined; }> = M[MapK<M>];
|
||||||
export type MapM<M extends { [key: string]: JsonValue; }> = {
|
|
||||||
[KK in MapK<M>]: M[KK];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A collaborative map with precise shape `M` and optional static metadata `Meta` */
|
/** A collaborative map with precise shape `M` and optional static metadata `Meta` */
|
||||||
export class CoMap<
|
export class CoMap<
|
||||||
M extends { [key: string]: JsonValue; },
|
M extends { [key: string]: JsonValue | undefined; },
|
||||||
Meta extends JsonObject | null = null,
|
Meta extends JsonObject | null = null,
|
||||||
> implements ReadableCoValue {
|
> implements ReadableCoValue {
|
||||||
id: CoID<CoMap<MapM<M>, Meta>>;
|
id: CoID<CoMap<M, Meta>>;
|
||||||
type = "comap" as const;
|
type = "comap" as const;
|
||||||
core: CoValueCore;
|
core: CoValueCore;
|
||||||
/** @internal */
|
/** @internal */
|
||||||
@@ -43,7 +42,7 @@ export class CoMap<
|
|||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
constructor(core: CoValueCore) {
|
constructor(core: CoValueCore) {
|
||||||
this.id = core.id as CoID<CoMap<MapM<M>, Meta>>;
|
this.id = core.id as CoID<CoMap<M, Meta>>;
|
||||||
this.core = core;
|
this.core = core;
|
||||||
this.ops = {};
|
this.ops = {};
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ export class CoMap<
|
|||||||
|
|
||||||
for (const { txID, changes, madeAt } of this.core.getValidSortedTransactions()) {
|
for (const { txID, changes, madeAt } of this.core.getValidSortedTransactions()) {
|
||||||
for (const [changeIdx, changeUntyped] of (
|
for (const [changeIdx, changeUntyped] of (
|
||||||
changes
|
parseJSON(changes)
|
||||||
).entries()) {
|
).entries()) {
|
||||||
const change = changeUntyped as MapOpPayload<MapK<M>, MapV<M>>;
|
const change = changeUntyped as MapOpPayload<MapK<M>, MapV<M>>;
|
||||||
let entries = this.ops[change.key];
|
let entries = this.ops[change.key];
|
||||||
@@ -207,7 +206,7 @@ export class CoMap<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class WriteableCoMap<
|
export class WriteableCoMap<
|
||||||
M extends { [key: string]: JsonValue; },
|
M extends { [key: string]: JsonValue | undefined; },
|
||||||
Meta extends JsonObject | null = null,
|
Meta extends JsonObject | null = null,
|
||||||
> extends CoMap<M, Meta> implements WriteableCoValue {
|
> extends CoMap<M, Meta> implements WriteableCoValue {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { JsonObject, JsonValue } from "../jsonValue.js";
|
import { JsonObject, JsonValue } from "../jsonValue.js";
|
||||||
import { CoID, ReadableCoValue, WriteableCoValue } from "../coValue.js";
|
import { CoID, ReadableCoValue, WriteableCoValue } from "../coValue.js";
|
||||||
import { CoValueCore } from "../coValueCore.js";
|
import { CoValueCore, accountOrAgentIDfromSessionID } from "../coValueCore.js";
|
||||||
import { Group } from "../group.js";
|
import { Group } from "../group.js";
|
||||||
import { SessionID } from "../ids.js";
|
import { SessionID } from "../ids.js";
|
||||||
import { base64url } from "@scure/base";
|
import { base64URLtoBytes, bytesToBase64url } from "../base64url.js";
|
||||||
|
import { AccountID } from "../index.js";
|
||||||
|
import { isAccountID } from "../account.js";
|
||||||
|
import { parseJSON } from "../jsonStringify.js";
|
||||||
|
|
||||||
export type BinaryChunkInfo = {
|
export type BinaryChunkInfo = {
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
@@ -17,7 +20,7 @@ export type BinaryStreamStart = {
|
|||||||
|
|
||||||
export type BinaryStreamChunk = {
|
export type BinaryStreamChunk = {
|
||||||
type: "chunk";
|
type: "chunk";
|
||||||
chunk: `U${string}`;
|
chunk: `binary_U${string}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BinaryStreamEnd = {
|
export type BinaryStreamEnd = {
|
||||||
@@ -40,13 +43,14 @@ export class CoStream<
|
|||||||
type = "costream" as const;
|
type = "costream" as const;
|
||||||
core: CoValueCore;
|
core: CoValueCore;
|
||||||
items: {
|
items: {
|
||||||
[key: SessionID]: T[];
|
[key: SessionID]: {item: T, madeAt: number}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(core: CoValueCore) {
|
constructor(core: CoValueCore) {
|
||||||
this.id = core.id as CoID<CoStream<T, Meta>>;
|
this.id = core.id as CoID<CoStream<T, Meta>>;
|
||||||
this.core = core;
|
this.core = core;
|
||||||
this.items = {};
|
this.items = {};
|
||||||
|
this.fillFromCoValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
get meta(): Meta {
|
get meta(): Meta {
|
||||||
@@ -63,16 +67,17 @@ export class CoStream<
|
|||||||
|
|
||||||
for (const {
|
for (const {
|
||||||
txID,
|
txID,
|
||||||
|
madeAt,
|
||||||
changes,
|
changes,
|
||||||
} of this.core.getValidSortedTransactions()) {
|
} of this.core.getValidSortedTransactions()) {
|
||||||
for (const changeUntyped of changes) {
|
for (const changeUntyped of parseJSON(changes)) {
|
||||||
const change = changeUntyped as T;
|
const change = changeUntyped as T;
|
||||||
let entries = this.items[txID.sessionID];
|
let entries = this.items[txID.sessionID];
|
||||||
if (!entries) {
|
if (!entries) {
|
||||||
entries = [];
|
entries = [];
|
||||||
this.items[txID.sessionID] = entries;
|
this.items[txID.sessionID] = entries;
|
||||||
}
|
}
|
||||||
entries.push(change);
|
entries.push({item: change, madeAt});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,13 +91,57 @@ export class CoStream<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.values(this.items)[0];
|
return Object.values(this.items)[0]?.map(item => item.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastItemsPerAccount(): {[account: AccountID]: T | undefined} {
|
||||||
|
const result: {[account: AccountID]: {item: T, madeAt: number} | undefined} = {};
|
||||||
|
|
||||||
|
for (const [sessionID, items] of Object.entries(this.items)) {
|
||||||
|
const account = accountOrAgentIDfromSessionID(sessionID as SessionID);
|
||||||
|
if (!isAccountID(account)) continue;
|
||||||
|
if (items.length > 0) {
|
||||||
|
const lastItemOfSession = items[items.length - 1]!;
|
||||||
|
if (!result[account] || lastItemOfSession.madeAt > result[account]!.madeAt) {
|
||||||
|
result[account] = lastItemOfSession;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.fromEntries(Object.entries(result).map(([account, item]) =>
|
||||||
|
[account, item?.item]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastItemFrom(account: AccountID): T | undefined {
|
||||||
|
let lastItem: {item: T, madeAt: number} | undefined;
|
||||||
|
|
||||||
|
for (const [sessionID, items] of Object.entries(this.items)) {
|
||||||
|
if (sessionID.startsWith(account)) {
|
||||||
|
if (items.length > 0) {
|
||||||
|
const lastItemOfSession = items[items.length - 1]!;
|
||||||
|
if (!lastItem || lastItemOfSession.madeAt > lastItem.madeAt) {
|
||||||
|
lastItem = lastItemOfSession;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastItem?.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastItemFromMe(): T | undefined {
|
||||||
|
const myAccountID = this.core.node.account.id;
|
||||||
|
if (!isAccountID(myAccountID)) return undefined;
|
||||||
|
return this.getLastItemFrom(myAccountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): {
|
toJSON(): {
|
||||||
[key: SessionID]: T[];
|
[key: SessionID]: T[];
|
||||||
} {
|
} {
|
||||||
return this.items;
|
return Object.fromEntries(Object.entries(this.items).map(([sessionID, items]) =>
|
||||||
|
[sessionID, items.map(item => item.item)]
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(listener: (coMap: CoStream<T, Meta>) => void): () => void {
|
subscribe(listener: (coMap: CoStream<T, Meta>) => void): () => void {
|
||||||
@@ -110,6 +159,8 @@ export class CoStream<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const binary_U_prefixLength = 8; // "binary_U".length;
|
||||||
|
|
||||||
export class BinaryCoStream<
|
export class BinaryCoStream<
|
||||||
Meta extends BinaryCoStreamMeta = { type: "binary" }
|
Meta extends BinaryCoStreamMeta = { type: "binary" }
|
||||||
>
|
>
|
||||||
@@ -118,9 +169,10 @@ export class BinaryCoStream<
|
|||||||
{
|
{
|
||||||
id!: CoID<BinaryCoStream<Meta>>;
|
id!: CoID<BinaryCoStream<Meta>>;
|
||||||
|
|
||||||
getBinaryChunks():
|
getBinaryChunks(allowUnfinished?: boolean):
|
||||||
| (BinaryChunkInfo & { chunks: Uint8Array[]; finished: boolean })
|
| (BinaryChunkInfo & { chunks: Uint8Array[]; finished: boolean })
|
||||||
| undefined {
|
| undefined {
|
||||||
|
// const before = performance.now();
|
||||||
const items = this.getSingleStream();
|
const items = this.getSingleStream();
|
||||||
|
|
||||||
if (!items) return;
|
if (!items) return;
|
||||||
@@ -132,17 +184,19 @@ export class BinaryCoStream<
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const end = items[items.length - 1];
|
||||||
|
|
||||||
|
if (end?.type !== "end" && !allowUnfinished) return;
|
||||||
|
|
||||||
const chunks: Uint8Array[] = [];
|
const chunks: Uint8Array[] = [];
|
||||||
|
|
||||||
|
let finished = false;
|
||||||
|
// let totalLength = 0;
|
||||||
|
|
||||||
for (const item of items.slice(1)) {
|
for (const item of items.slice(1)) {
|
||||||
if (item.type === "end") {
|
if (item.type === "end") {
|
||||||
return {
|
finished = true;
|
||||||
mimeType: start.mimeType,
|
break;
|
||||||
fileName: start.fileName,
|
|
||||||
totalSizeBytes: start.totalSizeBytes,
|
|
||||||
chunks,
|
|
||||||
finished: true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.type !== "chunk") {
|
if (item.type !== "chunk") {
|
||||||
@@ -150,15 +204,25 @@ export class BinaryCoStream<
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
chunks.push(base64url.decode(item.chunk.slice(1)));
|
const chunk = base64URLtoBytes(
|
||||||
|
item.chunk.slice(binary_U_prefixLength)
|
||||||
|
);
|
||||||
|
// totalLength += chunk.length;
|
||||||
|
chunks.push(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// const after = performance.now();
|
||||||
|
// console.log(
|
||||||
|
// "getBinaryChunks bandwidth in MB/s",
|
||||||
|
// (1000 * totalLength) / (after - before) / (1024 * 1024)
|
||||||
|
// );
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mimeType: start.mimeType,
|
mimeType: start.mimeType,
|
||||||
fileName: start.fileName,
|
fileName: start.fileName,
|
||||||
totalSizeBytes: start.totalSizeBytes,
|
totalSizeBytes: start.totalSizeBytes,
|
||||||
chunks,
|
chunks,
|
||||||
finished: false,
|
finished,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,10 +269,7 @@ export class WriteableBinaryCoStream<
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
push(
|
push(item: BinaryStreamItem, privacy: "private" | "trusting" = "private") {
|
||||||
item: BinaryStreamItem,
|
|
||||||
privacy: "private" | "trusting" = "private"
|
|
||||||
) {
|
|
||||||
WriteableCoStream.prototype.push.call(this, item, privacy);
|
WriteableCoStream.prototype.push.call(this, item, privacy);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,13 +290,19 @@ export class WriteableBinaryCoStream<
|
|||||||
chunk: Uint8Array,
|
chunk: Uint8Array,
|
||||||
privacy: "private" | "trusting" = "private"
|
privacy: "private" | "trusting" = "private"
|
||||||
) {
|
) {
|
||||||
|
// const before = performance.now();
|
||||||
this.push(
|
this.push(
|
||||||
{
|
{
|
||||||
type: "chunk",
|
type: "chunk",
|
||||||
chunk: `U${base64url.encode(chunk)}`,
|
chunk: `binary_U${bytesToBase64url(chunk)}`,
|
||||||
} satisfies BinaryStreamChunk,
|
} satisfies BinaryStreamChunk,
|
||||||
privacy
|
privacy
|
||||||
);
|
);
|
||||||
|
// const after = performance.now();
|
||||||
|
// console.log(
|
||||||
|
// "pushBinaryStreamChunk bandwidth in MB/s",
|
||||||
|
// (1000 * chunk.length) / (after - before) / (1024 * 1024)
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
endBinaryStream(privacy: "private" | "trusting" = "private") {
|
endBinaryStream(privacy: "private" | "trusting" = "private") {
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ import { xsalsa20_poly1305 } from "@noble/ciphers/salsa";
|
|||||||
import { blake3 } from "@noble/hashes/blake3";
|
import { blake3 } from "@noble/hashes/blake3";
|
||||||
import stableStringify from "fast-json-stable-stringify";
|
import stableStringify from "fast-json-stable-stringify";
|
||||||
import { SessionID } from './ids.js';
|
import { SessionID } from './ids.js';
|
||||||
|
import { cojsonReady } from './index.js';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await cojsonReady;
|
||||||
|
});
|
||||||
|
|
||||||
test("Signatures round-trip and use stable stringify", () => {
|
test("Signatures round-trip and use stable stringify", () => {
|
||||||
const data = { b: "world", a: "hello" };
|
const data = { b: "world", a: "hello" };
|
||||||
|
|||||||
@@ -1,11 +1,39 @@
|
|||||||
import { ed25519, x25519 } from "@noble/curves/ed25519";
|
import { ed25519, x25519 } from "@noble/curves/ed25519";
|
||||||
import { xsalsa20_poly1305, xsalsa20 } from "@noble/ciphers/salsa";
|
import { xsalsa20_poly1305, xsalsa20 } from "@noble/ciphers/salsa";
|
||||||
import { JsonValue } from "./jsonValue.js";
|
import { JsonValue } from "./jsonValue.js";
|
||||||
import { base58, base64url } from "@scure/base";
|
import { base58 } from "@scure/base";
|
||||||
import stableStringify from "fast-json-stable-stringify";
|
|
||||||
import { blake3 } from "@noble/hashes/blake3";
|
|
||||||
import { randomBytes } from "@noble/ciphers/webcrypto/utils";
|
import { randomBytes } from "@noble/ciphers/webcrypto/utils";
|
||||||
import { AgentID, RawCoID, TransactionID } from "./ids.js";
|
import { AgentID, RawCoID, TransactionID } from "./ids.js";
|
||||||
|
import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
|
||||||
|
|
||||||
|
import { createBLAKE3 } from 'hash-wasm';
|
||||||
|
import { Stringified, parseJSON, stableStringify } from "./jsonStringify.js";
|
||||||
|
|
||||||
|
let blake3Instance: Awaited<ReturnType<typeof createBLAKE3>>;
|
||||||
|
let blake3HashOnce: (data: Uint8Array) => Uint8Array;
|
||||||
|
let blake3HashOnceWithContext: (data: Uint8Array, {context}: {context: Uint8Array}) => Uint8Array;
|
||||||
|
let blake3incrementalUpdateSLOW_WITH_DEVTOOLS: (state: Uint8Array, data: Uint8Array) => Uint8Array;
|
||||||
|
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();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
export type SignerSecret = `signerSecret_z${string}`;
|
export type SignerSecret = `signerSecret_z${string}`;
|
||||||
export type SignerID = `signer_z${string}`;
|
export type SignerID = `signer_z${string}`;
|
||||||
@@ -127,7 +155,7 @@ export function seal<T extends JsonValue>(
|
|||||||
to: SealerID,
|
to: SealerID,
|
||||||
nOnceMaterial: { in: RawCoID; tx: TransactionID }
|
nOnceMaterial: { in: RawCoID; tx: TransactionID }
|
||||||
): Sealed<T> {
|
): Sealed<T> {
|
||||||
const nOnce = blake3(
|
const nOnce = blake3HashOnce(
|
||||||
textEncoder.encode(stableStringify(nOnceMaterial))
|
textEncoder.encode(stableStringify(nOnceMaterial))
|
||||||
).slice(0, 24);
|
).slice(0, 24);
|
||||||
|
|
||||||
@@ -143,7 +171,7 @@ export function seal<T extends JsonValue>(
|
|||||||
plaintext
|
plaintext
|
||||||
);
|
);
|
||||||
|
|
||||||
return `sealed_U${base64url.encode(sealedBytes)}` as Sealed<T>;
|
return `sealed_U${bytesToBase64url(sealedBytes)}` as Sealed<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unseal<T extends JsonValue>(
|
export function unseal<T extends JsonValue>(
|
||||||
@@ -152,7 +180,7 @@ export function unseal<T extends JsonValue>(
|
|||||||
from: SealerID,
|
from: SealerID,
|
||||||
nOnceMaterial: { in: RawCoID; tx: TransactionID }
|
nOnceMaterial: { in: RawCoID; tx: TransactionID }
|
||||||
): T | undefined {
|
): T | undefined {
|
||||||
const nOnce = blake3(
|
const nOnce = blake3HashOnce(
|
||||||
textEncoder.encode(stableStringify(nOnceMaterial))
|
textEncoder.encode(stableStringify(nOnceMaterial))
|
||||||
).slice(0, 24);
|
).slice(0, 24);
|
||||||
|
|
||||||
@@ -160,7 +188,7 @@ export function unseal<T extends JsonValue>(
|
|||||||
|
|
||||||
const senderPub = base58.decode(from.substring("sealer_z".length));
|
const senderPub = base58.decode(from.substring("sealer_z".length));
|
||||||
|
|
||||||
const sealedBytes = base64url.decode(sealed.substring("sealed_U".length));
|
const sealedBytes = base64URLtoBytes(sealed.substring("sealed_U".length));
|
||||||
|
|
||||||
const sharedSecret = x25519.getSharedSecret(sealerPriv, senderPub);
|
const sharedSecret = x25519.getSharedSecret(sealerPriv, senderPub);
|
||||||
|
|
||||||
@@ -180,28 +208,32 @@ export type Hash = `hash_z${string}`;
|
|||||||
|
|
||||||
export function secureHash(value: JsonValue): Hash {
|
export function secureHash(value: JsonValue): Hash {
|
||||||
return `hash_z${base58.encode(
|
return `hash_z${base58.encode(
|
||||||
blake3(textEncoder.encode(stableStringify(value)))
|
blake3HashOnce(textEncoder.encode(stableStringify(value)))
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StreamingHash {
|
export class StreamingHash {
|
||||||
state: ReturnType<typeof blake3.create>;
|
state: Uint8Array;
|
||||||
|
|
||||||
constructor(fromClone?: ReturnType<typeof blake3.create>) {
|
constructor(fromClone?: Uint8Array) {
|
||||||
this.state = fromClone || blake3.create({});
|
this.state = fromClone || blake3Instance.init().save();
|
||||||
}
|
}
|
||||||
|
|
||||||
update(value: JsonValue) {
|
update(value: JsonValue) {
|
||||||
this.state.update(textEncoder.encode(stableStringify(value)));
|
const encoded = textEncoder.encode(stableStringify(value))
|
||||||
|
// const before = performance.now();
|
||||||
|
this.state = blake3incrementalUpdateSLOW_WITH_DEVTOOLS(this.state, encoded);
|
||||||
|
// const after = performance.now();
|
||||||
|
// console.log(`Hashing throughput in MB/s`, 1000 * (encoded.length / (after - before)) / (1024 * 1024));
|
||||||
}
|
}
|
||||||
|
|
||||||
digest(): Hash {
|
digest(): Hash {
|
||||||
const hash = this.state.digest();
|
const hash = blake3digestForState(this.state);
|
||||||
return `hash_z${base58.encode(hash)}`;
|
return `hash_z${base58.encode(hash)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
clone(): StreamingHash {
|
clone(): StreamingHash {
|
||||||
return new StreamingHash(this.state.clone());
|
return new StreamingHash(new Uint8Array(this.state));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +242,10 @@ export const shortHashLength = 19;
|
|||||||
|
|
||||||
export function shortHash(value: JsonValue): ShortHash {
|
export function shortHash(value: JsonValue): ShortHash {
|
||||||
return `shortHash_z${base58.encode(
|
return `shortHash_z${base58.encode(
|
||||||
blake3(textEncoder.encode(stableStringify(value))).slice(0, shortHashLength)
|
blake3HashOnce(textEncoder.encode(stableStringify(value))).slice(
|
||||||
|
0,
|
||||||
|
shortHashLength
|
||||||
|
)
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,13 +272,13 @@ function encrypt<T extends JsonValue, N extends JsonValue>(
|
|||||||
const keySecretBytes = base58.decode(
|
const keySecretBytes = base58.decode(
|
||||||
keySecret.substring("keySecret_z".length)
|
keySecret.substring("keySecret_z".length)
|
||||||
);
|
);
|
||||||
const nOnce = blake3(
|
const nOnce = blake3HashOnce(
|
||||||
textEncoder.encode(stableStringify(nOnceMaterial))
|
textEncoder.encode(stableStringify(nOnceMaterial))
|
||||||
).slice(0, 24);
|
).slice(0, 24);
|
||||||
|
|
||||||
const plaintext = textEncoder.encode(stableStringify(value));
|
const plaintext = textEncoder.encode(stableStringify(value));
|
||||||
const ciphertext = xsalsa20(keySecretBytes, nOnce, plaintext);
|
const ciphertext = xsalsa20(keySecretBytes, nOnce, plaintext);
|
||||||
return `encrypted_U${base64url.encode(ciphertext)}` as Encrypted<T, N>;
|
return `encrypted_U${bytesToBase64url(ciphertext)}` as Encrypted<T, N>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encryptForTransaction<T extends JsonValue>(
|
export function encryptForTransaction<T extends JsonValue>(
|
||||||
@@ -281,30 +316,48 @@ export function encryptKeySecret(keys: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decryptRaw<T extends JsonValue, N extends JsonValue>(
|
||||||
|
encrypted: Encrypted<T, N>,
|
||||||
|
keySecret: KeySecret,
|
||||||
|
nOnceMaterial: N
|
||||||
|
): Stringified<T> {
|
||||||
|
const keySecretBytes = base58.decode(
|
||||||
|
keySecret.substring("keySecret_z".length)
|
||||||
|
);
|
||||||
|
const nOnce = blake3HashOnce(
|
||||||
|
textEncoder.encode(stableStringify(nOnceMaterial))
|
||||||
|
).slice(0, 24);
|
||||||
|
|
||||||
|
const ciphertext = base64URLtoBytes(
|
||||||
|
encrypted.substring("encrypted_U".length)
|
||||||
|
);
|
||||||
|
const plaintext = xsalsa20(keySecretBytes, nOnce, ciphertext);
|
||||||
|
|
||||||
|
return textDecoder.decode(plaintext) as Stringified<T>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
function decrypt<T extends JsonValue, N extends JsonValue>(
|
function decrypt<T extends JsonValue, N extends JsonValue>(
|
||||||
encrypted: Encrypted<T, N>,
|
encrypted: Encrypted<T, N>,
|
||||||
keySecret: KeySecret,
|
keySecret: KeySecret,
|
||||||
nOnceMaterial: N
|
nOnceMaterial: N
|
||||||
): T | undefined {
|
): T | undefined {
|
||||||
const keySecretBytes = base58.decode(
|
|
||||||
keySecret.substring("keySecret_z".length)
|
|
||||||
);
|
|
||||||
const nOnce = blake3(
|
|
||||||
textEncoder.encode(stableStringify(nOnceMaterial))
|
|
||||||
).slice(0, 24);
|
|
||||||
|
|
||||||
const ciphertext = base64url.decode(
|
|
||||||
encrypted.substring("encrypted_U".length)
|
|
||||||
);
|
|
||||||
const plaintext = xsalsa20(keySecretBytes, nOnce, ciphertext);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(textDecoder.decode(plaintext));
|
return parseJSON(decryptRaw(encrypted, keySecret, nOnceMaterial));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error("Decryption error", e)
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function decryptRawForTransaction<T extends JsonValue>(
|
||||||
|
encrypted: Encrypted<T, { in: RawCoID; tx: TransactionID }>,
|
||||||
|
keySecret: KeySecret,
|
||||||
|
nOnceMaterial: { in: RawCoID; tx: TransactionID }
|
||||||
|
): Stringified<T> | undefined {
|
||||||
|
return decryptRaw(encrypted, keySecret, nOnceMaterial);
|
||||||
|
}
|
||||||
|
|
||||||
export function decryptForTransaction<T extends JsonValue>(
|
export function decryptForTransaction<T extends JsonValue>(
|
||||||
encrypted: Encrypted<T, { in: RawCoID; tx: TransactionID }>,
|
encrypted: Encrypted<T, { in: RawCoID; tx: TransactionID }>,
|
||||||
keySecret: KeySecret,
|
keySecret: KeySecret,
|
||||||
@@ -355,15 +408,17 @@ export function newRandomSecretSeed(): Uint8Array {
|
|||||||
|
|
||||||
export function agentSecretFromSecretSeed(secretSeed: Uint8Array): AgentSecret {
|
export function agentSecretFromSecretSeed(secretSeed: Uint8Array): AgentSecret {
|
||||||
if (secretSeed.length !== secretSeedLength) {
|
if (secretSeed.length !== secretSeedLength) {
|
||||||
throw new Error(`Secret seed needs to be ${secretSeedLength} bytes long`);
|
throw new Error(
|
||||||
|
`Secret seed needs to be ${secretSeedLength} bytes long`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `sealerSecret_z${base58.encode(
|
return `sealerSecret_z${base58.encode(
|
||||||
blake3(secretSeed, {
|
blake3HashOnceWithContext(secretSeed, {
|
||||||
context: textEncoder.encode("seal"),
|
context: textEncoder.encode("seal"),
|
||||||
})
|
})
|
||||||
)}/signerSecret_z${base58.encode(
|
)}/signerSecret_z${base58.encode(
|
||||||
blake3(secretSeed, {
|
blake3HashOnceWithContext(secretSeed, {
|
||||||
context: textEncoder.encode("sign"),
|
context: textEncoder.encode("sign"),
|
||||||
})
|
})
|
||||||
)}`;
|
)}`;
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { LocalNode, CoMap, CoList, CoStream, BinaryCoStream } from "./index";
|
import { LocalNode, CoMap, CoList, CoStream, BinaryCoStream, cojsonReady } from "./index";
|
||||||
import { randomAnonymousAccountAndSessionID } from "./testUtils";
|
import { randomAnonymousAccountAndSessionID } from "./testUtils";
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await cojsonReady;
|
||||||
|
});
|
||||||
|
|
||||||
test("Can create a CoMap in a group", () => {
|
test("Can create a CoMap in a group", () => {
|
||||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
||||||
|
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ export class Group {
|
|||||||
|
|
||||||
/** Creates a new `CoMap` within this group, with the specified specialized
|
/** Creates a new `CoMap` within this group, with the specified specialized
|
||||||
* `CoMap` type `M` and optional static metadata. */
|
* `CoMap` type `M` and optional static metadata. */
|
||||||
createMap<M extends CoMap<{ [key: string]: JsonValue }, JsonObject | null>>(
|
createMap<M extends CoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>>(
|
||||||
meta?: M["meta"]
|
meta?: M["meta"]
|
||||||
): M {
|
): M {
|
||||||
return this.node
|
return this.node
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CoValueCore, newRandomSessionID } from "./coValueCore.js";
|
import { CoValueCore, newRandomSessionID, MAX_RECOMMENDED_TX_SIZE } from "./coValueCore.js";
|
||||||
import { LocalNode } from "./node.js";
|
import { LocalNode } from "./node.js";
|
||||||
import type { CoValue, ReadableCoValue } from "./coValue.js";
|
import type { CoValue, ReadableCoValue } from "./coValue.js";
|
||||||
import { CoMap, WriteableCoMap } from "./coValues/coMap.js";
|
import { CoMap, WriteableCoMap } from "./coValues/coMap.js";
|
||||||
@@ -18,11 +18,14 @@ import {
|
|||||||
agentSecretFromSecretSeed,
|
agentSecretFromSecretSeed,
|
||||||
secretSeedLength,
|
secretSeedLength,
|
||||||
shortHashLength,
|
shortHashLength,
|
||||||
|
cryptoReady
|
||||||
} from "./crypto.js";
|
} from "./crypto.js";
|
||||||
import { connectedPeers } from "./streamUtils.js";
|
import { connectedPeers } from "./streamUtils.js";
|
||||||
import { AnonymousControlledAccount, ControlledAccount } from "./account.js";
|
import { AnonymousControlledAccount, ControlledAccount } from "./account.js";
|
||||||
import { rawCoIDtoBytes, rawCoIDfromBytes } from "./ids.js";
|
import { rawCoIDtoBytes, rawCoIDfromBytes } from "./ids.js";
|
||||||
import { Group, expectGroupContent } from "./group.js";
|
import { Group, expectGroupContent } from "./group.js";
|
||||||
|
import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
|
||||||
|
import { parseJSON } from "./jsonStringify.js";
|
||||||
|
|
||||||
import type { SessionID, AgentID } from "./ids.js";
|
import type { SessionID, AgentID } from "./ids.js";
|
||||||
import type { CoID, CoValueImpl } from "./coValue.js";
|
import type { CoID, CoValueImpl } from "./coValue.js";
|
||||||
@@ -32,6 +35,7 @@ import type { SyncMessage, Peer } from "./sync.js";
|
|||||||
import type { AgentSecret } from "./crypto.js";
|
import type { AgentSecret } from "./crypto.js";
|
||||||
import type { AccountID, Profile } from "./account.js";
|
import type { AccountID, Profile } from "./account.js";
|
||||||
import type { InviteSecret } from "./group.js";
|
import type { InviteSecret } from "./group.js";
|
||||||
|
import type * as Media from "./media.js";
|
||||||
|
|
||||||
type Value = JsonValue | CoValueImpl;
|
type Value = JsonValue | CoValueImpl;
|
||||||
|
|
||||||
@@ -50,6 +54,9 @@ export const cojsonInternals = {
|
|||||||
secretSeedLength,
|
secretSeedLength,
|
||||||
shortHashLength,
|
shortHashLength,
|
||||||
expectGroupContent,
|
expectGroupContent,
|
||||||
|
base64URLtoBytes,
|
||||||
|
bytesToBase64url,
|
||||||
|
parseJSON
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -66,6 +73,8 @@ export {
|
|||||||
CoValueCore,
|
CoValueCore,
|
||||||
AnonymousControlledAccount,
|
AnonymousControlledAccount,
|
||||||
ControlledAccount,
|
ControlledAccount,
|
||||||
|
cryptoReady as cojsonReady,
|
||||||
|
MAX_RECOMMENDED_TX_SIZE
|
||||||
};
|
};
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
@@ -85,6 +94,7 @@ export type {
|
|||||||
AgentSecret,
|
AgentSecret,
|
||||||
InviteSecret,
|
InviteSecret,
|
||||||
SyncMessage,
|
SyncMessage,
|
||||||
|
Media
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
|||||||
66
packages/cojson/src/jsonStringify.ts
Normal file
66
packages/cojson/src/jsonStringify.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// adapted from fast-json-stable-stringify (https://github.com/epoberezkin/fast-json-stable-stringify)
|
||||||
|
|
||||||
|
export type Stringified<T> = string & { __type: T };
|
||||||
|
|
||||||
|
export function stableStringify<T>(data: T): Stringified<T>
|
||||||
|
export function stableStringify(data: undefined): undefined
|
||||||
|
export function stableStringify<T>(data: T | undefined): Stringified<T> | undefined {
|
||||||
|
const cycles = false;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const seen: any[] = [];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let node = data as any;
|
||||||
|
|
||||||
|
if (node && node.toJSON && typeof node.toJSON === "function") {
|
||||||
|
node = node.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node === undefined) return;
|
||||||
|
if (typeof node == "number")
|
||||||
|
return (isFinite(node) ? "" + node : "null") as Stringified<T>;
|
||||||
|
if (typeof node !== "object") {
|
||||||
|
if (
|
||||||
|
typeof node === "string" &&
|
||||||
|
(node.startsWith("encrypted_U") || node.startsWith("binary_U"))
|
||||||
|
) {
|
||||||
|
return `"${node}"` as Stringified<T>;
|
||||||
|
}
|
||||||
|
return JSON.stringify(node) as Stringified<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let i, out;
|
||||||
|
if (Array.isArray(node)) {
|
||||||
|
out = "[";
|
||||||
|
for (i = 0; i < node.length; i++) {
|
||||||
|
if (i) out += ",";
|
||||||
|
out += stableStringify(node[i]) || "null";
|
||||||
|
}
|
||||||
|
return (out + "]") as Stringified<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node === null) return "null" as Stringified<T>;
|
||||||
|
|
||||||
|
if (seen.indexOf(node) !== -1) {
|
||||||
|
if (cycles) return JSON.stringify("__cycle__") as Stringified<T>;
|
||||||
|
throw new TypeError("Converting circular structure to JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
const seenIndex = seen.push(node) - 1;
|
||||||
|
const keys = Object.keys(node).sort();
|
||||||
|
out = "";
|
||||||
|
for (i = 0; i < keys.length; i++) {
|
||||||
|
const key = keys[i]!;
|
||||||
|
const value = stableStringify(node[key]);
|
||||||
|
|
||||||
|
if (!value) continue;
|
||||||
|
if (out) out += ",";
|
||||||
|
out += JSON.stringify(key) + ":" + value;
|
||||||
|
}
|
||||||
|
seen.splice(seenIndex, 1);
|
||||||
|
return ("{" + out + "}") as Stringified<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseJSON<T>(json: Stringified<T>): T {
|
||||||
|
return JSON.parse(json);
|
||||||
|
}
|
||||||
@@ -3,4 +3,4 @@ import { RawCoID } from './ids.js';
|
|||||||
export type JsonAtom = string | number | boolean | null;
|
export type JsonAtom = string | number | boolean | null;
|
||||||
export type JsonValue = JsonAtom | JsonArray | JsonObject | RawCoID;
|
export type JsonValue = JsonAtom | JsonArray | JsonObject | RawCoID;
|
||||||
export type JsonArray = JsonValue[];
|
export type JsonArray = JsonValue[];
|
||||||
export type JsonObject = { [key: string]: JsonValue; };
|
export type JsonObject = { [key: string]: JsonValue | undefined; };
|
||||||
|
|||||||
9
packages/cojson/src/media.ts
Normal file
9
packages/cojson/src/media.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { CoMap } from './coValues/coMap.js'
|
||||||
|
import { CoID } from './coValue.js'
|
||||||
|
import { BinaryCoStream } from './coValues/coStream.js'
|
||||||
|
|
||||||
|
export type ImageDefinition = CoMap<{
|
||||||
|
originalSize: [number, number];
|
||||||
|
placeholderDataURL?: string;
|
||||||
|
[res: `${number}x${number}`]: CoID<BinaryCoStream>;
|
||||||
|
}>;
|
||||||
@@ -208,7 +208,7 @@ export class LocalNode {
|
|||||||
reject(
|
reject(
|
||||||
new Error("Couldn't find invite before timeout")
|
new Error("Couldn't find invite before timeout")
|
||||||
),
|
),
|
||||||
1000
|
2000
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ import {
|
|||||||
groupWithTwoAdmins,
|
groupWithTwoAdmins,
|
||||||
groupWithTwoAdminsHighLevel,
|
groupWithTwoAdminsHighLevel,
|
||||||
} from "./testUtils.js";
|
} from "./testUtils.js";
|
||||||
import { AnonymousControlledAccount } from "./index.js";
|
import { AnonymousControlledAccount, cojsonReady } from "./index.js";
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await cojsonReady;
|
||||||
|
});
|
||||||
|
|
||||||
test("Initial admin can add another admin to a group", () => {
|
test("Initial admin can add another admin to a group", () => {
|
||||||
groupWithTwoAdmins();
|
groupWithTwoAdmins();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
AccountID,
|
AccountID,
|
||||||
Profile,
|
Profile,
|
||||||
} from "./account.js";
|
} from "./account.js";
|
||||||
|
import { parseJSON } from "./jsonStringify.js";
|
||||||
|
|
||||||
export type PermissionsDef =
|
export type PermissionsDef =
|
||||||
| { type: "group"; initialAdmin: AccountID | AgentID }
|
| { type: "group"; initialAdmin: AccountID | AgentID }
|
||||||
@@ -76,11 +77,13 @@ export function determineValidTransactions(
|
|||||||
// console.log("before", { memberState, validTransactions });
|
// console.log("before", { memberState, validTransactions });
|
||||||
const transactor = accountOrAgentIDfromSessionID(sessionID);
|
const transactor = accountOrAgentIDfromSessionID(sessionID);
|
||||||
|
|
||||||
const change = tx.changes[0] as
|
const changes = parseJSON(tx.changes)
|
||||||
|
|
||||||
|
const change = changes[0] as
|
||||||
| MapOpPayload<AccountID | AgentID, Role>
|
| MapOpPayload<AccountID | AgentID, Role>
|
||||||
| MapOpPayload<"readKey", JsonValue>
|
| MapOpPayload<"readKey", JsonValue>
|
||||||
| MapOpPayload<"profile", CoID<Profile>>;
|
| MapOpPayload<"profile", CoID<Profile>>;
|
||||||
if (tx.changes.length !== 1) {
|
if (changes.length !== 1) {
|
||||||
console.warn("Group transaction must have exactly one change");
|
console.warn("Group transaction must have exactly one change");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,14 @@ export function connectedPeers(
|
|||||||
trace &&
|
trace &&
|
||||||
console.debug(
|
console.debug(
|
||||||
`${peer2id} -> ${peer1id}`,
|
`${peer2id} -> ${peer1id}`,
|
||||||
JSON.stringify(chunk, null, 2)
|
JSON.stringify(
|
||||||
|
chunk,
|
||||||
|
(k, v) =>
|
||||||
|
(k === "changes" || k === "encryptedChanges")
|
||||||
|
? v.slice(0, 20) + "..."
|
||||||
|
: v,
|
||||||
|
2
|
||||||
|
)
|
||||||
);
|
);
|
||||||
controller.enqueue(chunk);
|
controller.enqueue(chunk);
|
||||||
},
|
},
|
||||||
@@ -52,7 +59,14 @@ export function connectedPeers(
|
|||||||
trace &&
|
trace &&
|
||||||
console.debug(
|
console.debug(
|
||||||
`${peer1id} -> ${peer2id}`,
|
`${peer1id} -> ${peer2id}`,
|
||||||
JSON.stringify(chunk, null, 2)
|
JSON.stringify(
|
||||||
|
chunk,
|
||||||
|
(k, v) =>
|
||||||
|
(k === "changes" || k === "encryptedChanges")
|
||||||
|
? v.slice(0, 20) + "..."
|
||||||
|
: v,
|
||||||
|
2
|
||||||
|
)
|
||||||
);
|
);
|
||||||
controller.enqueue(chunk);
|
controller.enqueue(chunk);
|
||||||
},
|
},
|
||||||
@@ -102,16 +116,22 @@ export function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let lastWritePromise = Promise.resolve();
|
||||||
|
|
||||||
const writable = new WritableStream<T>({
|
const writable = new WritableStream<T>({
|
||||||
async write(chunk) {
|
async write(chunk) {
|
||||||
const enqueue = await enqueuePromise;
|
const enqueue = await enqueuePromise;
|
||||||
if (readerClosed) {
|
if (readerClosed) {
|
||||||
throw new Error("Reader closed");
|
throw new Error("Reader closed");
|
||||||
} else {
|
} else {
|
||||||
// make sure write resolves before corresponding read
|
// make sure write resolves before corresponding read, but make sure writes are still in order
|
||||||
setTimeout(() => {
|
await lastWritePromise;
|
||||||
enqueue(chunk);
|
lastWritePromise = new Promise((resolve) => {
|
||||||
})
|
setTimeout(() => {
|
||||||
|
enqueue(chunk);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async abort(reason) {
|
async abort(reason) {
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
import { newRandomSessionID } from "./coValueCore.js";
|
import { newRandomSessionID } from "./coValueCore.js";
|
||||||
import { LocalNode } from "./node.js";
|
import { LocalNode } from "./node.js";
|
||||||
import { Peer, PeerID, SyncMessage } from "./sync.js";
|
import { SyncMessage } from "./sync.js";
|
||||||
import { expectMap } from "./coValue.js";
|
import { expectMap } from "./coValue.js";
|
||||||
import { MapOpPayload } from "./coValues/coMap.js";
|
import { MapOpPayload } from "./coValues/coMap.js";
|
||||||
import { Group } from "./group.js";
|
import { Group } from "./group.js";
|
||||||
import {
|
|
||||||
ReadableStream,
|
|
||||||
WritableStream,
|
|
||||||
TransformStream,
|
|
||||||
} from "isomorphic-streams";
|
|
||||||
import {
|
import {
|
||||||
randomAnonymousAccountAndSessionID,
|
randomAnonymousAccountAndSessionID,
|
||||||
shouldNotResolve,
|
shouldNotResolve,
|
||||||
} from "./testUtils.js";
|
} from "./testUtils.js";
|
||||||
import {
|
import { connectedPeers, newStreamPair } from "./streamUtils.js";
|
||||||
connectedPeers,
|
|
||||||
newStreamPair
|
|
||||||
} from "./streamUtils.js";
|
|
||||||
import { AccountID } from "./account.js";
|
import { AccountID } from "./account.js";
|
||||||
|
import { cojsonReady } from "./index.js";
|
||||||
|
import { stableStringify } from "./jsonStringify.js";
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await cojsonReady;
|
||||||
|
});
|
||||||
|
|
||||||
test("Node replies with initial tx and header to empty subscribe", async () => {
|
test("Node replies with initial tx and header to empty subscribe", async () => {
|
||||||
const [admin, session] = randomAnonymousAccountAndSessionID();
|
const [admin, session] = randomAnonymousAccountAndSessionID();
|
||||||
@@ -84,13 +82,13 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
|
|||||||
privacy: "trusting" as const,
|
privacy: "trusting" as const,
|
||||||
madeAt: map.core.sessions[node.currentSessionID]!
|
madeAt: map.core.sessions[node.currentSessionID]!
|
||||||
.transactions[0]!.madeAt,
|
.transactions[0]!.madeAt,
|
||||||
changes: [
|
changes: stableStringify([
|
||||||
{
|
{
|
||||||
op: "set",
|
op: "set",
|
||||||
key: "hello",
|
key: "hello",
|
||||||
value: "world",
|
value: "world",
|
||||||
} satisfies MapOpPayload<string, string>,
|
} satisfies MapOpPayload<string, string>,
|
||||||
],
|
]),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
lastSignature:
|
lastSignature:
|
||||||
@@ -162,13 +160,13 @@ test("Node replies with only new tx to subscribe with some known state", async (
|
|||||||
privacy: "trusting" as const,
|
privacy: "trusting" as const,
|
||||||
madeAt: map.core.sessions[node.currentSessionID]!
|
madeAt: map.core.sessions[node.currentSessionID]!
|
||||||
.transactions[1]!.madeAt,
|
.transactions[1]!.madeAt,
|
||||||
changes: [
|
changes: stableStringify([
|
||||||
{
|
{
|
||||||
op: "set",
|
op: "set",
|
||||||
key: "goodbye",
|
key: "goodbye",
|
||||||
value: "world",
|
value: "world",
|
||||||
} satisfies MapOpPayload<string, string>,
|
} satisfies MapOpPayload<string, string>,
|
||||||
],
|
]),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
lastSignature:
|
lastSignature:
|
||||||
@@ -251,13 +249,13 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
|||||||
privacy: "trusting" as const,
|
privacy: "trusting" as const,
|
||||||
madeAt: map.core.sessions[node.currentSessionID]!
|
madeAt: map.core.sessions[node.currentSessionID]!
|
||||||
.transactions[0]!.madeAt,
|
.transactions[0]!.madeAt,
|
||||||
changes: [
|
changes: stableStringify([
|
||||||
{
|
{
|
||||||
op: "set",
|
op: "set",
|
||||||
key: "hello",
|
key: "hello",
|
||||||
value: "world",
|
value: "world",
|
||||||
} satisfies MapOpPayload<string, string>,
|
} satisfies MapOpPayload<string, string>,
|
||||||
],
|
]),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
lastSignature:
|
lastSignature:
|
||||||
@@ -283,13 +281,13 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
|||||||
privacy: "trusting" as const,
|
privacy: "trusting" as const,
|
||||||
madeAt: map.core.sessions[node.currentSessionID]!
|
madeAt: map.core.sessions[node.currentSessionID]!
|
||||||
.transactions[1]!.madeAt,
|
.transactions[1]!.madeAt,
|
||||||
changes: [
|
changes: stableStringify([
|
||||||
{
|
{
|
||||||
op: "set",
|
op: "set",
|
||||||
key: "goodbye",
|
key: "goodbye",
|
||||||
value: "world",
|
value: "world",
|
||||||
} satisfies MapOpPayload<string, string>,
|
} satisfies MapOpPayload<string, string>,
|
||||||
],
|
]),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
lastSignature:
|
lastSignature:
|
||||||
@@ -362,13 +360,13 @@ test("Client replies with known new content to tellKnownState from server", asyn
|
|||||||
privacy: "trusting" as const,
|
privacy: "trusting" as const,
|
||||||
madeAt: map.core.sessions[node.currentSessionID]!
|
madeAt: map.core.sessions[node.currentSessionID]!
|
||||||
.transactions[0]!.madeAt,
|
.transactions[0]!.madeAt,
|
||||||
changes: [
|
changes: stableStringify([
|
||||||
{
|
{
|
||||||
op: "set",
|
op: "set",
|
||||||
key: "hello",
|
key: "hello",
|
||||||
value: "world",
|
value: "world",
|
||||||
} satisfies MapOpPayload<string, string>,
|
} satisfies MapOpPayload<string, string>,
|
||||||
],
|
]),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
lastSignature:
|
lastSignature:
|
||||||
@@ -438,8 +436,9 @@ test("No matter the optimistic known state, node respects invalid known state me
|
|||||||
editable.set("goodbye", "world", "trusting");
|
editable.set("goodbye", "world", "trusting");
|
||||||
});
|
});
|
||||||
|
|
||||||
const _mapEditMsg1 = await reader.read();
|
const _mapEditMsgs = await reader.read();
|
||||||
const _mapEditMsg2 = await reader.read();
|
|
||||||
|
console.log("Sending correction");
|
||||||
|
|
||||||
await writer.write({
|
await writer.write({
|
||||||
action: "known",
|
action: "known",
|
||||||
@@ -465,13 +464,13 @@ test("No matter the optimistic known state, node respects invalid known state me
|
|||||||
privacy: "trusting" as const,
|
privacy: "trusting" as const,
|
||||||
madeAt: map.core.sessions[node.currentSessionID]!
|
madeAt: map.core.sessions[node.currentSessionID]!
|
||||||
.transactions[1]!.madeAt,
|
.transactions[1]!.madeAt,
|
||||||
changes: [
|
changes: stableStringify([
|
||||||
{
|
{
|
||||||
op: "set",
|
op: "set",
|
||||||
key: "goodbye",
|
key: "goodbye",
|
||||||
value: "world",
|
value: "world",
|
||||||
} satisfies MapOpPayload<string, string>,
|
} satisfies MapOpPayload<string, string>,
|
||||||
],
|
]),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
lastSignature:
|
lastSignature:
|
||||||
@@ -568,13 +567,13 @@ test("If we add a server peer, all updates to all coValues are sent to it, even
|
|||||||
privacy: "trusting" as const,
|
privacy: "trusting" as const,
|
||||||
madeAt: map.core.sessions[node.currentSessionID]!
|
madeAt: map.core.sessions[node.currentSessionID]!
|
||||||
.transactions[0]!.madeAt,
|
.transactions[0]!.madeAt,
|
||||||
changes: [
|
changes: stableStringify([
|
||||||
{
|
{
|
||||||
op: "set",
|
op: "set",
|
||||||
key: "hello",
|
key: "hello",
|
||||||
value: "world",
|
value: "world",
|
||||||
} satisfies MapOpPayload<string, string>,
|
} satisfies MapOpPayload<string, string>,
|
||||||
],
|
]),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
lastSignature:
|
lastSignature:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
WritableStreamDefaultWriter,
|
WritableStreamDefaultWriter,
|
||||||
} from "isomorphic-streams";
|
} from "isomorphic-streams";
|
||||||
import { RawCoID, SessionID } from "./ids.js";
|
import { RawCoID, SessionID } from "./ids.js";
|
||||||
|
import { stableStringify } from "./jsonStringify.js";
|
||||||
|
|
||||||
export type CoValueKnownState = {
|
export type CoValueKnownState = {
|
||||||
id: RawCoID;
|
id: RawCoID;
|
||||||
@@ -214,14 +215,32 @@ export class SyncManager {
|
|||||||
await this.sendNewContentIncludingDependencies(id, peer);
|
await this.sendNewContentIncludingDependencies(id, peer);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newContent = coValue.newContentSince(
|
const newContentPieces = coValue.newContentSince(
|
||||||
peer.optimisticKnownStates[id]
|
peer.optimisticKnownStates[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newContent) {
|
if (newContentPieces) {
|
||||||
await this.trySendToPeer(peer, newContent);
|
const optimisticKnownStateBefore =
|
||||||
|
peer.optimisticKnownStates[id] || emptyKnownState(id);
|
||||||
|
|
||||||
|
const sendPieces = async () => {
|
||||||
|
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)
|
||||||
|
// );
|
||||||
|
await this.trySendToPeer(peer, piece);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sendPieces().catch((e) => {
|
||||||
|
console.error("Error sending new content piece, retrying", e);
|
||||||
|
peer.optimisticKnownStates[id] = optimisticKnownStateBefore;
|
||||||
|
return this.sendNewContentIncludingDependencies(id, peer);
|
||||||
|
});
|
||||||
|
|
||||||
peer.optimisticKnownStates[id] = combinedKnownStates(
|
peer.optimisticKnownStates[id] = combinedKnownStates(
|
||||||
peer.optimisticKnownStates[id] || emptyKnownState(id),
|
optimisticKnownStateBefore,
|
||||||
coValue.knownState()
|
coValue.knownState()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -260,6 +279,9 @@ export class SyncManager {
|
|||||||
for await (const msg of peerState.incoming) {
|
for await (const msg of peerState.incoming) {
|
||||||
try {
|
try {
|
||||||
await this.handleSyncMessage(msg, peerState);
|
await this.handleSyncMessage(msg, peerState);
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
setTimeout(resolve, 0);
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(
|
||||||
`Error reading from peer ${peer.id}, handling msg`,
|
`Error reading from peer ${peer.id}, handling msg`,
|
||||||
@@ -268,7 +290,6 @@ export class SyncManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("DONE!!!");
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error reading from peer ${peer.id}`, e);
|
console.error(`Error reading from peer ${peer.id}`, e);
|
||||||
}
|
}
|
||||||
@@ -445,15 +466,39 @@ export class SyncManager {
|
|||||||
const newTransactions =
|
const newTransactions =
|
||||||
newContentForSession.newTransactions.slice(alreadyKnownOffset);
|
newContentForSession.newTransactions.slice(alreadyKnownOffset);
|
||||||
|
|
||||||
const success = coValue.tryAddTransactions(
|
if (newTransactions.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const before = performance.now();
|
||||||
|
const success = await coValue.tryAddTransactionsAsync(
|
||||||
sessionID,
|
sessionID,
|
||||||
newTransactions,
|
newTransactions,
|
||||||
undefined,
|
undefined,
|
||||||
newContentForSession.lastSignature
|
newContentForSession.lastSignature
|
||||||
);
|
);
|
||||||
|
const after = performance.now();
|
||||||
|
if (after - before > 10) {
|
||||||
|
const totalTxLength = newTransactions
|
||||||
|
.map((t) =>
|
||||||
|
t.privacy === "private"
|
||||||
|
? t.encryptedChanges.length
|
||||||
|
: t.changes.length
|
||||||
|
)
|
||||||
|
.reduce((a, b) => a + b, 0);
|
||||||
|
console.log(
|
||||||
|
`Adding incoming transactions took ${(
|
||||||
|
after - before
|
||||||
|
).toFixed(2)}ms for ${totalTxLength} bytes = bandwidth: ${(
|
||||||
|
(1000 * totalTxLength) /
|
||||||
|
(after - before) /
|
||||||
|
(1024 * 1024)
|
||||||
|
).toFixed(2)} MB/s`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
console.error("Failed to add transactions", newTransactions);
|
console.error("Failed to add transactions", msg.id, newTransactions);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,18 +523,9 @@ export class SyncManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleCorrection(msg: KnownStateMessage, peer: PeerState) {
|
async handleCorrection(msg: KnownStateMessage, peer: PeerState) {
|
||||||
const coValue = this.local.expectCoValueLoaded(msg.id);
|
peer.optimisticKnownStates[msg.id] = msg;
|
||||||
|
|
||||||
peer.optimisticKnownStates[msg.id] = combinedKnownStates(
|
return this.sendNewContentIncludingDependencies(msg.id, peer);
|
||||||
msg,
|
|
||||||
coValue.knownState()
|
|
||||||
);
|
|
||||||
|
|
||||||
const newContent = coValue.newContentSince(msg);
|
|
||||||
|
|
||||||
if (newContent) {
|
|
||||||
await this.trySendToPeer(peer, newContent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnsubscribe(_msg: DoneMessage) {
|
handleUnsubscribe(_msg: DoneMessage) {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "jazz-browser-auth-local",
|
"name": "jazz-browser-auth-local",
|
||||||
"version": "0.1.10",
|
"version": "0.2.2",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jazz-browser": "^0.1.10",
|
"jazz-browser": "^0.2.2",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ async function signUp(
|
|||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
id: webAuthNCredentialPayload,
|
id: webAuthNCredentialPayload,
|
||||||
name: username + `(${new Date().toLocaleString()})`,
|
name: username + ` (${new Date().toLocaleString()})`,
|
||||||
displayName: username,
|
displayName: username,
|
||||||
},
|
},
|
||||||
pubKeyCredParams: [{ alg: -7, type: "public-key" }],
|
pubKeyCredParams: [{ alg: -7, type: "public-key" }],
|
||||||
|
|||||||
17
packages/jazz-browser-media-images/.eslintrc.cjs
Normal file
17
packages/jazz-browser-media-images/.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/jazz-browser-media-images/.gitignore
vendored
Normal file
171
packages/jazz-browser-media-images/.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/jazz-browser-media-images/.npmignore
Normal file
2
packages/jazz-browser-media-images/.npmignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
coverage
|
||||||
|
node_modules
|
||||||
21
packages/jazz-browser-media-images/package.json
Normal file
21
packages/jazz-browser-media-images/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "jazz-browser-media-images",
|
||||||
|
"version": "0.2.2",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cojson": "^0.2.1",
|
||||||
|
"image-blob-reduce": "^4.1.0",
|
||||||
|
"jazz-browser": "^0.2.2",
|
||||||
|
"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/image-blob-reduce": "^4.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
333
packages/jazz-browser-media-images/src/index.ts
Normal file
333
packages/jazz-browser-media-images/src/index.ts
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
import { CoID, Group, LocalNode, Media } from "cojson";
|
||||||
|
|
||||||
|
import ImageBlobReduce from "image-blob-reduce";
|
||||||
|
import Pica from "pica";
|
||||||
|
import {
|
||||||
|
createBinaryStreamFromBlob,
|
||||||
|
readBlobFromBinaryStream,
|
||||||
|
} from "jazz-browser";
|
||||||
|
|
||||||
|
const pica = new Pica();
|
||||||
|
|
||||||
|
export async function createImage(
|
||||||
|
image: Blob | File,
|
||||||
|
inGroup: Group
|
||||||
|
): Promise<Media.ImageDefinition> {
|
||||||
|
let originalWidth!: number;
|
||||||
|
let originalHeight!: number;
|
||||||
|
const Reducer = new ImageBlobReduce({ pica });
|
||||||
|
Reducer.after("_blob_to_image", (env) => {
|
||||||
|
originalWidth =
|
||||||
|
(env as unknown as { orientation: number }).orientation & 4
|
||||||
|
? env.image.height
|
||||||
|
: env.image.width;
|
||||||
|
originalHeight =
|
||||||
|
(env as unknown as { orientation: number }).orientation & 4
|
||||||
|
? env.image.width
|
||||||
|
: env.image.height;
|
||||||
|
return Promise.resolve(env);
|
||||||
|
});
|
||||||
|
|
||||||
|
const placeholderDataURL = (
|
||||||
|
await Reducer.toCanvas(image, { max: 8 })
|
||||||
|
).toDataURL("image/png");
|
||||||
|
|
||||||
|
let imageDefinition = inGroup.createMap<Media.ImageDefinition>();
|
||||||
|
|
||||||
|
imageDefinition = imageDefinition.edit((imageDefinition) => {
|
||||||
|
imageDefinition.set("originalSize", [originalWidth, originalHeight]);
|
||||||
|
imageDefinition.set("placeholderDataURL", placeholderDataURL);
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
const max256 = await Reducer.toBlob(image, { max: 256 });
|
||||||
|
|
||||||
|
if (originalWidth > 256 || originalHeight > 256) {
|
||||||
|
const width =
|
||||||
|
originalWidth > originalHeight
|
||||||
|
? 256
|
||||||
|
: Math.round(256 * (originalWidth / originalHeight));
|
||||||
|
const height =
|
||||||
|
originalHeight > originalWidth
|
||||||
|
? 256
|
||||||
|
: Math.round(256 * (originalHeight / originalWidth));
|
||||||
|
|
||||||
|
const binaryStreamId = (
|
||||||
|
await createBinaryStreamFromBlob(max256, inGroup)
|
||||||
|
).id;
|
||||||
|
|
||||||
|
imageDefinition.edit((imageDefinition) => {
|
||||||
|
imageDefinition.set(`${width}x${height}`, binaryStreamId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
const max1024 = await Reducer.toBlob(image, { max: 1024 });
|
||||||
|
|
||||||
|
if (originalWidth > 1024 || originalHeight > 1024) {
|
||||||
|
const width =
|
||||||
|
originalWidth > originalHeight
|
||||||
|
? 1024
|
||||||
|
: Math.round(1024 * (originalWidth / originalHeight));
|
||||||
|
const height =
|
||||||
|
originalHeight > originalWidth
|
||||||
|
? 1024
|
||||||
|
: Math.round(1024 * (originalHeight / originalWidth));
|
||||||
|
|
||||||
|
const binaryStreamId = (
|
||||||
|
await createBinaryStreamFromBlob(max1024, inGroup)
|
||||||
|
).id;
|
||||||
|
|
||||||
|
imageDefinition.edit((imageDefinition) => {
|
||||||
|
imageDefinition.set(`${width}x${height}`, binaryStreamId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
const max2048 = await Reducer.toBlob(image, { max: 2048 });
|
||||||
|
|
||||||
|
if (originalWidth > 2048 || originalHeight > 2048) {
|
||||||
|
const width =
|
||||||
|
originalWidth > originalHeight
|
||||||
|
? 2048
|
||||||
|
: Math.round(2048 * (originalWidth / originalHeight));
|
||||||
|
const height =
|
||||||
|
originalHeight > originalWidth
|
||||||
|
? 2048
|
||||||
|
: Math.round(2048 * (originalHeight / originalWidth));
|
||||||
|
|
||||||
|
const binaryStreamId = (
|
||||||
|
await createBinaryStreamFromBlob(max2048, inGroup)
|
||||||
|
).id;
|
||||||
|
|
||||||
|
imageDefinition.edit((imageDefinition) => {
|
||||||
|
imageDefinition.set(`${width}x${height}`, binaryStreamId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
const originalBinaryStreamId = (
|
||||||
|
await createBinaryStreamFromBlob(image, inGroup)
|
||||||
|
).id;
|
||||||
|
|
||||||
|
imageDefinition.edit((imageDefinition) => {
|
||||||
|
imageDefinition.set(
|
||||||
|
`${originalWidth}x${originalHeight}`,
|
||||||
|
originalBinaryStreamId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return imageDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoadingImageInfo = {
|
||||||
|
originalSize?: [number, number];
|
||||||
|
placeholderDataURL?: string;
|
||||||
|
highestResSrc?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function loadImage(
|
||||||
|
imageID: CoID<Media.ImageDefinition>,
|
||||||
|
localNode: LocalNode,
|
||||||
|
progressiveCallback: (update: LoadingImageInfo) => void
|
||||||
|
): () => void {
|
||||||
|
let unsubscribe: (() => void) | undefined;
|
||||||
|
let stopped = false;
|
||||||
|
|
||||||
|
const resState: {
|
||||||
|
[res: `${number}x${number}`]:
|
||||||
|
| { state: "queued" }
|
||||||
|
| { state: "waiting" }
|
||||||
|
| { state: "loading"; doneOrFailed: Promise<void> }
|
||||||
|
| { state: "loaded"; blobURL: string }
|
||||||
|
| { state: "revoked" }
|
||||||
|
| { state: "failed" }
|
||||||
|
| undefined;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
const cleanUp = () => {
|
||||||
|
stopped = true;
|
||||||
|
for (const [res, entry] of Object.entries(resState)) {
|
||||||
|
if (entry?.state === "loaded") {
|
||||||
|
URL.revokeObjectURL(entry.blobURL);
|
||||||
|
resState[res as `${number}x${number}`] = { state: "revoked" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unsubscribe?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
localNode
|
||||||
|
.load(imageID)
|
||||||
|
.then((imageDefinition) => {
|
||||||
|
if (stopped) return;
|
||||||
|
unsubscribe = imageDefinition.subscribe(async (imageDefinition) => {
|
||||||
|
if (stopped) return;
|
||||||
|
|
||||||
|
const originalSize = imageDefinition.get("originalSize");
|
||||||
|
const placeholderDataURL =
|
||||||
|
imageDefinition.get("placeholderDataURL");
|
||||||
|
|
||||||
|
const resolutions = imageDefinition
|
||||||
|
.keys()
|
||||||
|
.filter(
|
||||||
|
(key): key is `${number}x${number}` =>
|
||||||
|
!!key.match(/\d+x\d+/)
|
||||||
|
)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const widthA = Number(a.split("x")[0]);
|
||||||
|
const widthB = Number(b.split("x")[0]);
|
||||||
|
return widthA - widthB;
|
||||||
|
});
|
||||||
|
|
||||||
|
const startLoading = async () => {
|
||||||
|
const notYetQueuedOrLoading = resolutions.filter(
|
||||||
|
(res) => !resState[res]
|
||||||
|
);
|
||||||
|
|
||||||
|
// console.log(
|
||||||
|
// "Loading iteration",
|
||||||
|
// resolutions,
|
||||||
|
// resState,
|
||||||
|
// notYetQueuedOrLoading
|
||||||
|
// );
|
||||||
|
|
||||||
|
for (const res of notYetQueuedOrLoading) {
|
||||||
|
resState[res] = { state: "queued" };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const res of notYetQueuedOrLoading) {
|
||||||
|
if (stopped) return;
|
||||||
|
resState[res] = { state: "waiting" };
|
||||||
|
|
||||||
|
const binaryStreamId = imageDefinition.get(res)!;
|
||||||
|
// console.log(
|
||||||
|
// "Loading image res",
|
||||||
|
// imageID,
|
||||||
|
// res,
|
||||||
|
// binaryStreamId
|
||||||
|
// );
|
||||||
|
|
||||||
|
const binaryStream = await localNode.load(
|
||||||
|
binaryStreamId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stopped) return;
|
||||||
|
if (!binaryStream) {
|
||||||
|
resState[res] = { state: "failed" };
|
||||||
|
console.error(
|
||||||
|
"Loading image res failed",
|
||||||
|
imageID,
|
||||||
|
res,
|
||||||
|
binaryStreamId
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise<void>((resolveFullyLoaded) => {
|
||||||
|
const unsubFromStream = binaryStream.subscribe(
|
||||||
|
async (_) => {
|
||||||
|
if (stopped) return;
|
||||||
|
const currentState = resState[res];
|
||||||
|
if (currentState?.state === "loading") {
|
||||||
|
await currentState.doneOrFailed;
|
||||||
|
// console.log(
|
||||||
|
// "Retrying image res after previous attempt",
|
||||||
|
// imageID,
|
||||||
|
// res,
|
||||||
|
// binaryStreamId
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
if (resState[res]?.state === "loaded") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const doneOrFailed = new Promise<void>(
|
||||||
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
|
async (resolveDoneOrFailed) => {
|
||||||
|
const blob =
|
||||||
|
await readBlobFromBinaryStream(
|
||||||
|
binaryStreamId,
|
||||||
|
localNode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stopped) return;
|
||||||
|
if (!blob) {
|
||||||
|
// console.log(
|
||||||
|
// "Image res not available yet",
|
||||||
|
// imageID,
|
||||||
|
// res,
|
||||||
|
// binaryStreamId
|
||||||
|
// );
|
||||||
|
resolveDoneOrFailed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blobURL =
|
||||||
|
URL.createObjectURL(blob);
|
||||||
|
resState[res] = {
|
||||||
|
state: "loaded",
|
||||||
|
blobURL,
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.log(
|
||||||
|
// "Loaded image res",
|
||||||
|
// imageID,
|
||||||
|
// res,
|
||||||
|
// binaryStreamId
|
||||||
|
// );
|
||||||
|
|
||||||
|
progressiveCallback({
|
||||||
|
originalSize,
|
||||||
|
placeholderDataURL,
|
||||||
|
highestResSrc: blobURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
unsubFromStream();
|
||||||
|
resolveDoneOrFailed();
|
||||||
|
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
resolveFullyLoaded();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
resState[res] = {
|
||||||
|
state: "loading",
|
||||||
|
doneOrFailed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Object.values(resState).some(
|
||||||
|
(entry) => entry?.state === "loaded"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
progressiveCallback({
|
||||||
|
originalSize,
|
||||||
|
placeholderDataURL,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startLoading().catch((err) => {
|
||||||
|
console.error("Error loading image", imageID, err);
|
||||||
|
cleanUp();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Error loading image", imageID, err);
|
||||||
|
cleanUp();
|
||||||
|
});
|
||||||
|
|
||||||
|
return cleanUp;
|
||||||
|
}
|
||||||
16
packages/jazz-browser-media-images/tsconfig.json
Normal file
16
packages/jazz-browser-media-images/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"module": "esnext",
|
||||||
|
"target": "ES2020",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
},
|
||||||
|
"include": ["./src/**/*"],
|
||||||
|
}
|
||||||
75
packages/jazz-browser-media-images/yarn.lock
Normal file
75
packages/jazz-browser-media-images/yarn.lock
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@noble/ciphers@^0.1.3":
|
||||||
|
version "0.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.1.4.tgz#96327dca147829ed9eee0d96cfdf7c57915765f0"
|
||||||
|
integrity sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==
|
||||||
|
|
||||||
|
"@noble/curves@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d"
|
||||||
|
integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==
|
||||||
|
dependencies:
|
||||||
|
"@noble/hashes" "1.3.1"
|
||||||
|
|
||||||
|
"@noble/hashes@1.3.1", "@noble/hashes@^1.3.1":
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
|
||||||
|
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
|
||||||
|
|
||||||
|
"@scure/base@^1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
|
||||||
|
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
|
||||||
|
|
||||||
|
"@types/prop-types@*":
|
||||||
|
version "15.7.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||||
|
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
||||||
|
|
||||||
|
"@types/react@^18.2.19":
|
||||||
|
version "18.2.19"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.19.tgz#f77cb2c8307368e624d464a25b9675fa35f95a8b"
|
||||||
|
integrity sha512-e2S8wmY1ePfM517PqCG80CcE48Xs5k0pwJzuDZsfE8IZRRBfOMCF+XqnFxu6mWtyivum1MQm4aco+WIt6Coimw==
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types" "*"
|
||||||
|
"@types/scheduler" "*"
|
||||||
|
csstype "^3.0.2"
|
||||||
|
|
||||||
|
"@types/scheduler@*":
|
||||||
|
version "0.16.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
||||||
|
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
|
||||||
|
|
||||||
|
cojson@^0.0.14:
|
||||||
|
version "0.0.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/cojson/-/cojson-0.0.14.tgz#e7b190ade1efc20d6f0fa12411d7208cdc0f19f7"
|
||||||
|
integrity sha512-TFenIGswEEhnZlCmq+B1NZPztjovZ72AjK1YkkZca54ZFbB1lAHdPt2hqqu/QBO24C9+6DtuoS2ixm6gbSBWCg==
|
||||||
|
dependencies:
|
||||||
|
"@noble/ciphers" "^0.1.3"
|
||||||
|
"@noble/curves" "^1.1.0"
|
||||||
|
"@noble/hashes" "^1.3.1"
|
||||||
|
"@scure/base" "^1.1.1"
|
||||||
|
fast-json-stable-stringify "^2.1.0"
|
||||||
|
isomorphic-streams "https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
|
||||||
|
|
||||||
|
csstype@^3.0.2:
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||||
|
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||||
|
|
||||||
|
fast-json-stable-stringify@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||||
|
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
||||||
|
|
||||||
|
"isomorphic-streams@git+https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae":
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "git+https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
|
||||||
|
|
||||||
|
typescript@^5.1.6:
|
||||||
|
version "5.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
|
||||||
|
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "jazz-browser",
|
"name": "jazz-browser",
|
||||||
"version": "0.1.10",
|
"version": "0.2.2",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cojson": "^0.1.10",
|
"cojson": "^0.2.1",
|
||||||
"jazz-storage-indexeddb": "^0.1.10",
|
"jazz-storage-indexeddb": "^0.2.2",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { BinaryCoStream, InviteSecret } from "cojson";
|
import { BinaryCoStream, InviteSecret } from "cojson";
|
||||||
import { BinaryCoStreamMeta } from "cojson";
|
import { BinaryCoStreamMeta } from "cojson";
|
||||||
|
import { MAX_RECOMMENDED_TX_SIZE } from "cojson";
|
||||||
|
import { cojsonReady } from "cojson";
|
||||||
import {
|
import {
|
||||||
LocalNode,
|
LocalNode,
|
||||||
cojsonInternals,
|
cojsonInternals,
|
||||||
@@ -30,6 +32,7 @@ export async function createBrowserNode({
|
|||||||
syncAddress?: string;
|
syncAddress?: string;
|
||||||
reconnectionTimeout?: number;
|
reconnectionTimeout?: number;
|
||||||
}): Promise<BrowserNodeHandle> {
|
}): Promise<BrowserNodeHandle> {
|
||||||
|
await cojsonReady;
|
||||||
let sessionDone: () => void;
|
let sessionDone: () => void;
|
||||||
|
|
||||||
const firstWsPeer = createWebSocketPeer(syncAddress);
|
const firstWsPeer = createWebSocketPeer(syncAddress);
|
||||||
@@ -70,6 +73,10 @@ export async function createBrowserNode({
|
|||||||
node,
|
node,
|
||||||
done: () => {
|
done: () => {
|
||||||
shouldTryToReconnect = false;
|
shouldTryToReconnect = false;
|
||||||
|
console.log("Cleaning up node")
|
||||||
|
for (const peer of Object.values(node.sync.peers)) {
|
||||||
|
peer.outgoing.close().catch(e => console.error("Error while closing peer", e));
|
||||||
|
}
|
||||||
sessionDone?.();
|
sessionDone?.();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -91,9 +98,7 @@ export type SessionHandle = {
|
|||||||
done: () => void;
|
done: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSessionHandleFor(
|
function getSessionHandleFor(accountID: AccountID | AgentID): SessionHandle {
|
||||||
accountID: AccountID | AgentID
|
|
||||||
): SessionHandle {
|
|
||||||
let done!: () => void;
|
let done!: () => void;
|
||||||
const donePromise = new Promise<void>((resolve) => {
|
const donePromise = new Promise<void>((resolve) => {
|
||||||
done = resolve;
|
done = resolve;
|
||||||
@@ -176,15 +181,25 @@ function websocketReadableStream<T>(ws: WebSocket) {
|
|||||||
|
|
||||||
pingTimeout = setTimeout(() => {
|
pingTimeout = setTimeout(() => {
|
||||||
console.debug("Ping timeout");
|
console.debug("Ping timeout");
|
||||||
controller.close();
|
try {
|
||||||
ws.close();
|
controller.close();
|
||||||
|
ws.close();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
"Error while trying to close ws on ping timeout",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
}, 2500);
|
}, 2500);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
controller.enqueue(msg);
|
controller.enqueue(msg);
|
||||||
};
|
};
|
||||||
const closeListener = () => controller.close();
|
const closeListener = () => {
|
||||||
|
controller.close();
|
||||||
|
clearTimeout(pingTimeout);
|
||||||
|
};
|
||||||
ws.addEventListener("close", closeListener);
|
ws.addEventListener("close", closeListener);
|
||||||
ws.addEventListener("error", () => {
|
ws.addEventListener("error", () => {
|
||||||
controller.error(new Error("The WebSocket errored!"));
|
controller.error(new Error("The WebSocket errored!"));
|
||||||
@@ -305,7 +320,9 @@ export function createInviteLink(
|
|||||||
return `${baseURL}#invitedTo=${value.id}&${inviteSecret}`;
|
return `${baseURL}#invitedTo=${value.id}&${inviteSecret}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseInviteLink<C extends CoValueImpl>(inviteURL: string):
|
export function parseInviteLink<C extends CoValueImpl>(
|
||||||
|
inviteURL: string
|
||||||
|
):
|
||||||
| {
|
| {
|
||||||
valueID: CoID<C>;
|
valueID: CoID<C>;
|
||||||
inviteSecret: InviteSecret;
|
inviteSecret: InviteSecret;
|
||||||
@@ -322,7 +339,9 @@ export function parseInviteLink<C extends CoValueImpl>(inviteURL: string):
|
|||||||
return { valueID, inviteSecret };
|
return { valueID, inviteSecret };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function consumeInviteLinkFromWindowLocation<C extends CoValueImpl>(node: LocalNode): Promise<
|
export function consumeInviteLinkFromWindowLocation<C extends CoValueImpl>(
|
||||||
|
node: LocalNode
|
||||||
|
): Promise<
|
||||||
| {
|
| {
|
||||||
valueID: CoID<C>;
|
valueID: CoID<C>;
|
||||||
inviteSecret: string;
|
inviteSecret: string;
|
||||||
@@ -349,28 +368,39 @@ export function consumeInviteLinkFromWindowLocation<C extends CoValueImpl>(node:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createBinaryStreamFromBlob<C extends BinaryCoStream<BinaryCoStreamMeta>>(blob: Blob | File, inGroup: Group, meta: C["meta"] = {type: "binary"}): Promise<C> {
|
export async function createBinaryStreamFromBlob<
|
||||||
const stream = inGroup.createBinaryStream(meta);
|
C extends BinaryCoStream<BinaryCoStreamMeta>
|
||||||
|
>(
|
||||||
|
blob: Blob | File,
|
||||||
|
inGroup: Group,
|
||||||
|
meta: C["meta"] = { type: "binary" }
|
||||||
|
): Promise<C> {
|
||||||
|
let stream = inGroup.createBinaryStream(meta);
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
const done = new Promise<void>((resolve) => {
|
const done = new Promise<void>((resolve) => {
|
||||||
|
reader.onload = async () => {
|
||||||
reader.onload = () => {
|
|
||||||
const data = new Uint8Array(reader.result as ArrayBuffer);
|
const data = new Uint8Array(reader.result as ArrayBuffer);
|
||||||
stream.edit(stream => {
|
stream = stream.edit((stream) => {
|
||||||
stream.startBinaryStream({
|
stream.startBinaryStream({
|
||||||
mimeType: blob.type,
|
mimeType: blob.type,
|
||||||
totalSizeBytes: blob.size,
|
totalSizeBytes: blob.size,
|
||||||
fileName: blob instanceof File ? blob.name : undefined,
|
fileName: blob instanceof File ? blob.name : undefined,
|
||||||
});
|
});
|
||||||
const chunkSize = 100 * 1024;
|
}) as C;// TODO: fix this
|
||||||
|
const chunkSize = MAX_RECOMMENDED_TX_SIZE;
|
||||||
|
|
||||||
for (let idx = 0; idx < data.length; idx += chunkSize) {
|
for (let idx = 0; idx < data.length; idx += chunkSize) {
|
||||||
stream.pushBinaryStreamChunk(data.slice(idx, idx + chunkSize));
|
stream = stream.edit((stream) => {
|
||||||
|
stream.pushBinaryStreamChunk(
|
||||||
|
data.slice(idx, idx + chunkSize)
|
||||||
|
);
|
||||||
|
}) as C; // TODO: fix this
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
}
|
}
|
||||||
|
stream = stream.edit((stream) => {
|
||||||
stream.endBinaryStream();
|
stream.endBinaryStream();
|
||||||
});
|
}) as C; // TODO: fix this
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -381,22 +411,24 @@ export async function createBinaryStreamFromBlob<C extends BinaryCoStream<Binary
|
|||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readBlobFromBinaryStream<C extends BinaryCoStream<BinaryCoStreamMeta>>(streamId: CoID<C>, node: LocalNode, allowUnfinished?: boolean): Promise<Blob | undefined> {
|
export async function readBlobFromBinaryStream<
|
||||||
|
C extends BinaryCoStream<BinaryCoStreamMeta>
|
||||||
|
>(
|
||||||
|
streamId: CoID<C>,
|
||||||
|
node: LocalNode,
|
||||||
|
allowUnfinished?: boolean
|
||||||
|
): Promise<Blob | undefined> {
|
||||||
const stream = await node.load<C>(streamId);
|
const stream = await node.load<C>(streamId);
|
||||||
|
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunks = stream.getBinaryChunks();
|
const chunks = stream.getBinaryChunks(allowUnfinished);
|
||||||
|
|
||||||
if (!chunks) {
|
if (!chunks) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allowUnfinished && !chunks.finished) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Blob(chunks.chunks, { type: chunks.mimeType });
|
return new Blob(chunks.chunks, { type: chunks.mimeType });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "jazz-react-auth-local",
|
"name": "jazz-react-auth-local",
|
||||||
"version": "0.1.12",
|
"version": "0.2.2",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jazz-browser-auth-local": "^0.1.10",
|
"jazz-browser-auth-local": "^0.2.2",
|
||||||
"jazz-react": "^0.1.12",
|
"jazz-react": "^0.2.2",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
17
packages/jazz-react-media-images/.eslintrc.cjs
Normal file
17
packages/jazz-react-media-images/.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/jazz-react-media-images/.gitignore
vendored
Normal file
171
packages/jazz-react-media-images/.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/jazz-react-media-images/.npmignore
Normal file
2
packages/jazz-react-media-images/.npmignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
coverage
|
||||||
|
node_modules
|
||||||
26
packages/jazz-react-media-images/package.json
Normal file
26
packages/jazz-react-media-images/package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "jazz-react-media-images",
|
||||||
|
"version": "0.2.2",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cojson": "^0.2.1",
|
||||||
|
"jazz-browser": "^0.2.2",
|
||||||
|
"jazz-browser-media-images": "^0.2.2",
|
||||||
|
"jazz-react": "^0.2.2",
|
||||||
|
"typescript": "^5.1.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.19"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "17 - 18"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint src/**/*.tsx",
|
||||||
|
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13"
|
||||||
|
}
|
||||||
24
packages/jazz-react-media-images/src/index.tsx
Normal file
24
packages/jazz-react-media-images/src/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { CoID, Media } from "cojson";
|
||||||
|
import { loadImage, LoadingImageInfo } from "jazz-browser-media-images";
|
||||||
|
import { useJazz } from "jazz-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
export { createImage } from "jazz-browser-media-images";
|
||||||
|
|
||||||
|
export function useLoadImage(
|
||||||
|
imageID?: CoID<Media.ImageDefinition>
|
||||||
|
): LoadingImageInfo | undefined {
|
||||||
|
const { localNode } = useJazz();
|
||||||
|
|
||||||
|
const [imageInfo, setImageInfo] = useState<LoadingImageInfo>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!imageID) return;
|
||||||
|
const unsubscribe = loadImage(imageID, localNode, (imageInfo) => {
|
||||||
|
setImageInfo(imageInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
}, [imageID, localNode]);
|
||||||
|
|
||||||
|
return imageInfo;
|
||||||
|
}
|
||||||
16
packages/jazz-react-media-images/tsconfig.json
Normal file
16
packages/jazz-react-media-images/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"module": "esnext",
|
||||||
|
"target": "ES2020",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
},
|
||||||
|
"include": ["./src/**/*"],
|
||||||
|
}
|
||||||
75
packages/jazz-react-media-images/yarn.lock
Normal file
75
packages/jazz-react-media-images/yarn.lock
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@noble/ciphers@^0.1.3":
|
||||||
|
version "0.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.1.4.tgz#96327dca147829ed9eee0d96cfdf7c57915765f0"
|
||||||
|
integrity sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==
|
||||||
|
|
||||||
|
"@noble/curves@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d"
|
||||||
|
integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==
|
||||||
|
dependencies:
|
||||||
|
"@noble/hashes" "1.3.1"
|
||||||
|
|
||||||
|
"@noble/hashes@1.3.1", "@noble/hashes@^1.3.1":
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
|
||||||
|
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
|
||||||
|
|
||||||
|
"@scure/base@^1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
|
||||||
|
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
|
||||||
|
|
||||||
|
"@types/prop-types@*":
|
||||||
|
version "15.7.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||||
|
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
||||||
|
|
||||||
|
"@types/react@^18.2.19":
|
||||||
|
version "18.2.19"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.19.tgz#f77cb2c8307368e624d464a25b9675fa35f95a8b"
|
||||||
|
integrity sha512-e2S8wmY1ePfM517PqCG80CcE48Xs5k0pwJzuDZsfE8IZRRBfOMCF+XqnFxu6mWtyivum1MQm4aco+WIt6Coimw==
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types" "*"
|
||||||
|
"@types/scheduler" "*"
|
||||||
|
csstype "^3.0.2"
|
||||||
|
|
||||||
|
"@types/scheduler@*":
|
||||||
|
version "0.16.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
||||||
|
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
|
||||||
|
|
||||||
|
cojson@^0.0.14:
|
||||||
|
version "0.0.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/cojson/-/cojson-0.0.14.tgz#e7b190ade1efc20d6f0fa12411d7208cdc0f19f7"
|
||||||
|
integrity sha512-TFenIGswEEhnZlCmq+B1NZPztjovZ72AjK1YkkZca54ZFbB1lAHdPt2hqqu/QBO24C9+6DtuoS2ixm6gbSBWCg==
|
||||||
|
dependencies:
|
||||||
|
"@noble/ciphers" "^0.1.3"
|
||||||
|
"@noble/curves" "^1.1.0"
|
||||||
|
"@noble/hashes" "^1.3.1"
|
||||||
|
"@scure/base" "^1.1.1"
|
||||||
|
fast-json-stable-stringify "^2.1.0"
|
||||||
|
isomorphic-streams "https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
|
||||||
|
|
||||||
|
csstype@^3.0.2:
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||||
|
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||||
|
|
||||||
|
fast-json-stable-stringify@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||||
|
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
||||||
|
|
||||||
|
"isomorphic-streams@git+https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae":
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "git+https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
|
||||||
|
|
||||||
|
typescript@^5.1.6:
|
||||||
|
version "5.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
|
||||||
|
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "jazz-react",
|
"name": "jazz-react",
|
||||||
"version": "0.1.12",
|
"version": "0.2.2",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cojson": "^0.1.10",
|
"cojson": "^0.2.1",
|
||||||
"jazz-browser": "^0.1.10",
|
"jazz-browser": "^0.2.2",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -8,14 +8,9 @@ import {
|
|||||||
CojsonInternalTypes,
|
CojsonInternalTypes,
|
||||||
BinaryCoStream,
|
BinaryCoStream,
|
||||||
BinaryCoStreamMeta,
|
BinaryCoStreamMeta,
|
||||||
Group,
|
|
||||||
} from "cojson";
|
} from "cojson";
|
||||||
import React, { ChangeEvent, useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import { AuthProvider, createBrowserNode } from "jazz-browser";
|
||||||
AuthProvider,
|
|
||||||
createBinaryStreamFromBlob,
|
|
||||||
createBrowserNode,
|
|
||||||
} from "jazz-browser";
|
|
||||||
import { readBlobFromBinaryStream } from "jazz-browser";
|
import { readBlobFromBinaryStream } from "jazz-browser";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -52,6 +47,7 @@ export function WithJazz({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let done: (() => void) | undefined = undefined;
|
let done: (() => void) | undefined = undefined;
|
||||||
|
let stop = false;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const nodeHandle = await createBrowserNode({
|
const nodeHandle = await createBrowserNode({
|
||||||
@@ -62,6 +58,11 @@ export function WithJazz({
|
|||||||
undefined,
|
undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (stop) {
|
||||||
|
nodeHandle.done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setNode(nodeHandle.node);
|
setNode(nodeHandle.node);
|
||||||
|
|
||||||
done = nodeHandle.done;
|
done = nodeHandle.done;
|
||||||
@@ -70,6 +71,7 @@ export function WithJazz({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
stop = true;
|
||||||
done && done();
|
done && done();
|
||||||
};
|
};
|
||||||
}, [auth, syncAddress]);
|
}, [auth, syncAddress]);
|
||||||
@@ -158,7 +160,7 @@ export function useProfile<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useBinaryStream<C extends BinaryCoStream<BinaryCoStreamMeta>>(
|
export function useBinaryStream<C extends BinaryCoStream<BinaryCoStreamMeta>>(
|
||||||
streamID: CoID<C>,
|
streamID?: CoID<C>,
|
||||||
allowUnfinished?: boolean
|
allowUnfinished?: boolean
|
||||||
): { blob: Blob; blobURL: string } | undefined {
|
): { blob: Blob; blobURL: string } | undefined {
|
||||||
const { localNode } = useJazz();
|
const { localNode } = useJazz();
|
||||||
@@ -171,32 +173,23 @@ export function useBinaryStream<C extends BinaryCoStream<BinaryCoStreamMeta>>(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!stream) return;
|
if (!stream) return;
|
||||||
readBlobFromBinaryStream(stream.id, localNode, allowUnfinished).then(
|
readBlobFromBinaryStream(stream.id, localNode, allowUnfinished)
|
||||||
(blob) =>
|
.then((blob) =>
|
||||||
setBlob(blob && {
|
setBlob(
|
||||||
blob,
|
blob && {
|
||||||
blobURL: URL.createObjectURL(blob),
|
blob,
|
||||||
})
|
blobURL: URL.createObjectURL(blob),
|
||||||
);
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.catch((e) => console.error("Failed to read binary stream", e));
|
||||||
}, [stream, localNode]);
|
}, [stream, localNode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
blob && URL.revokeObjectURL(blob.blobURL);
|
||||||
|
};
|
||||||
|
}, [blob?.blobURL]);
|
||||||
|
|
||||||
return blob;
|
return blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCreateBinaryStreamHandler<
|
|
||||||
C extends BinaryCoStream<BinaryCoStreamMeta>
|
|
||||||
>(
|
|
||||||
onCreated: (createdStream: C) => void,
|
|
||||||
inGroup: Group,
|
|
||||||
meta: C["meta"]
|
|
||||||
): (event: ChangeEvent) => void {
|
|
||||||
return async (event) => {
|
|
||||||
const file = (event.target as HTMLInputElement).files?.[0];
|
|
||||||
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const stream = await createBinaryStreamFromBlob(file, inGroup, meta);
|
|
||||||
|
|
||||||
onCreated(stream);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "jazz-storage-indexeddb",
|
"name": "jazz-storage-indexeddb",
|
||||||
"version": "0.1.10",
|
"version": "0.2.2",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cojson": "^0.1.10",
|
"cojson": "^0.2.1",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { cojsonInternals, SessionID, SyncMessage, Peer, CojsonInternalTypes } from "cojson";
|
import {
|
||||||
|
cojsonInternals,
|
||||||
|
SessionID,
|
||||||
|
SyncMessage,
|
||||||
|
Peer,
|
||||||
|
CojsonInternalTypes,
|
||||||
|
MAX_RECOMMENDED_TX_SIZE,
|
||||||
|
} from "cojson";
|
||||||
|
import { Signature } from "cojson/dist/crypto";
|
||||||
import {
|
import {
|
||||||
ReadableStream,
|
ReadableStream,
|
||||||
WritableStream,
|
WritableStream,
|
||||||
@@ -18,6 +26,7 @@ type SessionRow = {
|
|||||||
sessionID: SessionID;
|
sessionID: SessionID;
|
||||||
lastIdx: number;
|
lastIdx: number;
|
||||||
lastSignature: CojsonInternalTypes.Signature;
|
lastSignature: CojsonInternalTypes.Signature;
|
||||||
|
bytesSinceLastSignature?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StoredSessionRow = SessionRow & { rowID: number };
|
type StoredSessionRow = SessionRow & { rowID: number };
|
||||||
@@ -28,6 +37,12 @@ type TransactionRow = {
|
|||||||
tx: CojsonInternalTypes.Transaction;
|
tx: CojsonInternalTypes.Transaction;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SignatureAfterRow = {
|
||||||
|
ses: number;
|
||||||
|
idx: number;
|
||||||
|
signature: CojsonInternalTypes.Signature;
|
||||||
|
};
|
||||||
|
|
||||||
export class IDBStorage {
|
export class IDBStorage {
|
||||||
db: IDBDatabase;
|
db: IDBDatabase;
|
||||||
fromLocalNode!: ReadableStreamDefaultReader<SyncMessage>;
|
fromLocalNode!: ReadableStreamDefaultReader<SyncMessage>;
|
||||||
@@ -49,7 +64,7 @@ export class IDBStorage {
|
|||||||
done = result.done;
|
done = result.done;
|
||||||
|
|
||||||
if (result.value) {
|
if (result.value) {
|
||||||
this.handleSyncMessage(result.value);
|
await this.handleSyncMessage(result.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -82,42 +97,63 @@ export class IDBStorage {
|
|||||||
toLocalNode: WritableStream<SyncMessage>
|
toLocalNode: WritableStream<SyncMessage>
|
||||||
) {
|
) {
|
||||||
const dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
|
const dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
|
||||||
const request = indexedDB.open("jazz-storage", 1);
|
const request = indexedDB.open("jazz-storage", 4);
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
reject(request.error);
|
reject(request.error);
|
||||||
};
|
};
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
resolve(request.result);
|
resolve(request.result);
|
||||||
};
|
};
|
||||||
request.onupgradeneeded = () => {
|
request.onupgradeneeded = async (ev) => {
|
||||||
const db = request.result;
|
const db = request.result;
|
||||||
|
if (ev.oldVersion === 0) {
|
||||||
|
const coValues = db.createObjectStore("coValues", {
|
||||||
|
autoIncrement: true,
|
||||||
|
keyPath: "rowID",
|
||||||
|
});
|
||||||
|
|
||||||
const coValues = db.createObjectStore("coValues", {
|
coValues.createIndex("coValuesById", "id", {
|
||||||
autoIncrement: true,
|
|
||||||
keyPath: "rowID",
|
|
||||||
});
|
|
||||||
|
|
||||||
coValues.createIndex("coValuesById", "id", {
|
|
||||||
unique: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sessions = db.createObjectStore("sessions", {
|
|
||||||
autoIncrement: true,
|
|
||||||
keyPath: "rowID",
|
|
||||||
});
|
|
||||||
|
|
||||||
sessions.createIndex("sessionsByCoValue", "coValue");
|
|
||||||
sessions.createIndex(
|
|
||||||
"uniqueSessions",
|
|
||||||
["coValue", "sessionID"],
|
|
||||||
{
|
|
||||||
unique: true,
|
unique: true,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
db.createObjectStore("transactions", {
|
const sessions = db.createObjectStore("sessions", {
|
||||||
keyPath: ["ses", "idx"],
|
autoIncrement: true,
|
||||||
});
|
keyPath: "rowID",
|
||||||
|
});
|
||||||
|
|
||||||
|
sessions.createIndex("sessionsByCoValue", "coValue");
|
||||||
|
sessions.createIndex(
|
||||||
|
"uniqueSessions",
|
||||||
|
["coValue", "sessionID"],
|
||||||
|
{
|
||||||
|
unique: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
db.createObjectStore("transactions", {
|
||||||
|
keyPath: ["ses", "idx"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (ev.oldVersion <= 1) {
|
||||||
|
db.createObjectStore("signatureAfter", {
|
||||||
|
keyPath: ["ses", "idx"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (ev.oldVersion !== 0 && ev.oldVersion <= 3) {
|
||||||
|
// fix embarrassing off-by-one error for transaction indices
|
||||||
|
console.log("Migration: fixing off-by-one error");
|
||||||
|
const transaction = (ev.target as unknown as {transaction: IDBTransaction}).transaction;
|
||||||
|
|
||||||
|
const txsStore = transaction.objectStore("transactions");
|
||||||
|
const txs = await promised(txsStore.getAll());
|
||||||
|
|
||||||
|
for (const tx of txs) {
|
||||||
|
await promised(txsStore.delete([tx.ses, tx.idx]));
|
||||||
|
tx.idx -= 1;
|
||||||
|
await promised(txsStore.add(tx));
|
||||||
|
}
|
||||||
|
console.log("Migration: fixing off-by-one error - done");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -147,10 +183,12 @@ export class IDBStorage {
|
|||||||
coValues,
|
coValues,
|
||||||
sessions,
|
sessions,
|
||||||
transactions,
|
transactions,
|
||||||
|
signatureAfter,
|
||||||
}: {
|
}: {
|
||||||
coValues: IDBObjectStore;
|
coValues: IDBObjectStore;
|
||||||
sessions: IDBObjectStore;
|
sessions: IDBObjectStore;
|
||||||
transactions: IDBObjectStore;
|
transactions: IDBObjectStore;
|
||||||
|
signatureAfter: IDBObjectStore;
|
||||||
},
|
},
|
||||||
asDependencyOf?: CojsonInternalTypes.RawCoID
|
asDependencyOf?: CojsonInternalTypes.RawCoID
|
||||||
) {
|
) {
|
||||||
@@ -170,12 +208,14 @@ export class IDBStorage {
|
|||||||
sessions: {},
|
sessions: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const newContent: CojsonInternalTypes.NewContentMessage = {
|
const newContentPieces: CojsonInternalTypes.NewContentMessage[] = [
|
||||||
action: "content",
|
{
|
||||||
id: theirKnown.id,
|
action: "content",
|
||||||
header: theirKnown.header ? undefined : coValueRow?.header,
|
id: theirKnown.id,
|
||||||
new: {},
|
header: theirKnown.header ? undefined : coValueRow?.header,
|
||||||
};
|
new: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
for (const sessionRow of allOurSessions) {
|
for (const sessionRow of allOurSessions) {
|
||||||
ourKnown.sessions[sessionRow.sessionID] = sessionRow.lastIdx;
|
ourKnown.sessions[sessionRow.sessionID] = sessionRow.lastIdx;
|
||||||
@@ -187,6 +227,21 @@ export class IDBStorage {
|
|||||||
const firstNewTxIdx =
|
const firstNewTxIdx =
|
||||||
theirKnown.sessions[sessionRow.sessionID] || 0;
|
theirKnown.sessions[sessionRow.sessionID] || 0;
|
||||||
|
|
||||||
|
const signaturesAndIdxs = await promised<SignatureAfterRow[]>(
|
||||||
|
signatureAfter.getAll(
|
||||||
|
IDBKeyRange.bound(
|
||||||
|
[sessionRow.rowID, firstNewTxIdx],
|
||||||
|
[sessionRow.rowID, Infinity]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
theirKnown.id,
|
||||||
|
"signaturesAndIdxs",
|
||||||
|
JSON.stringify(signaturesAndIdxs)
|
||||||
|
);
|
||||||
|
|
||||||
const newTxInSession = await promised<TransactionRow[]>(
|
const newTxInSession = await promised<TransactionRow[]>(
|
||||||
transactions.getAll(
|
transactions.getAll(
|
||||||
IDBKeyRange.bound(
|
IDBKeyRange.bound(
|
||||||
@@ -196,36 +251,83 @@ export class IDBStorage {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
newContent.new[sessionRow.sessionID] = {
|
let idx = firstNewTxIdx;
|
||||||
after: firstNewTxIdx,
|
|
||||||
lastSignature: sessionRow.lastSignature,
|
console.log(
|
||||||
newTransactions: newTxInSession.map((row) => row.tx),
|
theirKnown.id,
|
||||||
};
|
"newTxInSession",
|
||||||
|
newTxInSession.length
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const tx of newTxInSession) {
|
||||||
|
let sessionEntry =
|
||||||
|
newContentPieces[newContentPieces.length - 1]!.new[
|
||||||
|
sessionRow.sessionID
|
||||||
|
];
|
||||||
|
if (!sessionEntry) {
|
||||||
|
sessionEntry = {
|
||||||
|
after: idx,
|
||||||
|
lastSignature: "WILL_BE_REPLACED" as Signature,
|
||||||
|
newTransactions: [],
|
||||||
|
};
|
||||||
|
newContentPieces[newContentPieces.length - 1]!.new[
|
||||||
|
sessionRow.sessionID
|
||||||
|
] = sessionEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionEntry.newTransactions.push(tx.tx);
|
||||||
|
|
||||||
|
if (
|
||||||
|
signaturesAndIdxs[0] &&
|
||||||
|
idx === signaturesAndIdxs[0].idx
|
||||||
|
) {
|
||||||
|
sessionEntry.lastSignature =
|
||||||
|
signaturesAndIdxs[0].signature;
|
||||||
|
signaturesAndIdxs.shift();
|
||||||
|
newContentPieces.push({
|
||||||
|
action: "content",
|
||||||
|
id: theirKnown.id,
|
||||||
|
new: {},
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
idx ===
|
||||||
|
firstNewTxIdx + newTxInSession.length - 1
|
||||||
|
) {
|
||||||
|
sessionEntry.lastSignature = sessionRow.lastSignature;
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dependedOnCoValues =
|
const dependedOnCoValues =
|
||||||
coValueRow?.header.ruleset.type === "group"
|
coValueRow?.header.ruleset.type === "group"
|
||||||
? Object.values(newContent.new).flatMap((sessionEntry) =>
|
? newContentPieces
|
||||||
sessionEntry.newTransactions.flatMap((tx) => {
|
.flatMap((piece) => Object.values(piece.new))
|
||||||
if (tx.privacy !== "trusting") return [];
|
.flatMap((sessionEntry) =>
|
||||||
return tx.changes
|
sessionEntry.newTransactions.flatMap((tx) => {
|
||||||
.map(
|
if (tx.privacy !== "trusting") return [];
|
||||||
(change) =>
|
// TODO: avoid parse here?
|
||||||
change &&
|
return cojsonInternals
|
||||||
typeof change === "object" &&
|
.parseJSON(tx.changes)
|
||||||
"op" in change &&
|
.map(
|
||||||
change.op === "set" &&
|
(change) =>
|
||||||
"key" in change &&
|
change &&
|
||||||
change.key
|
typeof change === "object" &&
|
||||||
)
|
"op" in change &&
|
||||||
.filter(
|
change.op === "set" &&
|
||||||
(key): key is CojsonInternalTypes.RawCoID =>
|
"key" in change &&
|
||||||
typeof key === "string" &&
|
change.key
|
||||||
key.startsWith("co_")
|
)
|
||||||
);
|
.filter(
|
||||||
})
|
(
|
||||||
)
|
key
|
||||||
|
): key is CojsonInternalTypes.RawCoID =>
|
||||||
|
typeof key === "string" &&
|
||||||
|
key.startsWith("co_")
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)
|
||||||
: coValueRow?.header.ruleset.type === "ownedByGroup"
|
: coValueRow?.header.ruleset.type === "ownedByGroup"
|
||||||
? [coValueRow?.header.ruleset.group]
|
? [coValueRow?.header.ruleset.group]
|
||||||
: [];
|
: [];
|
||||||
@@ -233,7 +335,7 @@ export class IDBStorage {
|
|||||||
for (const dependedOnCoValue of dependedOnCoValues) {
|
for (const dependedOnCoValue of dependedOnCoValues) {
|
||||||
await this.sendNewContentAfter(
|
await this.sendNewContentAfter(
|
||||||
{ id: dependedOnCoValue, header: false, sessions: {} },
|
{ id: dependedOnCoValue, header: false, sessions: {} },
|
||||||
{ coValues, sessions, transactions },
|
{ coValues, sessions, transactions, signatureAfter },
|
||||||
asDependencyOf || theirKnown.id
|
asDependencyOf || theirKnown.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -244,8 +346,15 @@ export class IDBStorage {
|
|||||||
asDependencyOf,
|
asDependencyOf,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newContent.header || Object.keys(newContent.new).length > 0) {
|
const nonEmptyNewContentPieces = newContentPieces.filter(
|
||||||
await this.toLocalNode.write(newContent);
|
(piece) => piece.header || Object.keys(piece.new).length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(theirKnown.id, nonEmptyNewContentPieces);
|
||||||
|
|
||||||
|
for (const piece of nonEmptyNewContentPieces) {
|
||||||
|
await this.toLocalNode.write(piece);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +363,7 @@ export class IDBStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleContent(msg: CojsonInternalTypes.NewContentMessage) {
|
async handleContent(msg: CojsonInternalTypes.NewContentMessage) {
|
||||||
const { coValues, sessions, transactions } =
|
const { coValues, sessions, transactions, signatureAfter } =
|
||||||
this.inTransaction("readwrite");
|
this.inTransaction("readwrite");
|
||||||
|
|
||||||
let storedCoValueRowID = (
|
let storedCoValueRowID = (
|
||||||
@@ -325,18 +434,39 @@ export class IDBStorage {
|
|||||||
const actuallyNewOffset =
|
const actuallyNewOffset =
|
||||||
(sessionRow?.lastIdx || 0) -
|
(sessionRow?.lastIdx || 0) -
|
||||||
(msg.new[sessionID]?.after || 0);
|
(msg.new[sessionID]?.after || 0);
|
||||||
|
|
||||||
const actuallyNewTransactions =
|
const actuallyNewTransactions =
|
||||||
newTransactions.slice(actuallyNewOffset);
|
newTransactions.slice(actuallyNewOffset);
|
||||||
|
|
||||||
|
let newBytesSinceLastSignature =
|
||||||
|
(sessionRow?.bytesSinceLastSignature || 0) +
|
||||||
|
actuallyNewTransactions.reduce(
|
||||||
|
(sum, tx) =>
|
||||||
|
sum +
|
||||||
|
(tx.privacy === "private"
|
||||||
|
? tx.encryptedChanges.length
|
||||||
|
: tx.changes.length),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const newLastIdx =
|
||||||
|
(sessionRow?.lastIdx || 0) + actuallyNewTransactions.length;
|
||||||
|
|
||||||
|
let shouldWriteSignature = false;
|
||||||
|
|
||||||
|
if (newBytesSinceLastSignature > MAX_RECOMMENDED_TX_SIZE) {
|
||||||
|
shouldWriteSignature = true;
|
||||||
|
newBytesSinceLastSignature = 0;
|
||||||
|
}
|
||||||
|
|
||||||
let nextIdx = sessionRow?.lastIdx || 0;
|
let nextIdx = sessionRow?.lastIdx || 0;
|
||||||
|
|
||||||
const sessionUpdate = {
|
const sessionUpdate = {
|
||||||
coValue: storedCoValueRowID,
|
coValue: storedCoValueRowID,
|
||||||
sessionID: sessionID,
|
sessionID: sessionID,
|
||||||
lastIdx:
|
lastIdx: newLastIdx,
|
||||||
(sessionRow?.lastIdx || 0) +
|
|
||||||
actuallyNewTransactions.length,
|
|
||||||
lastSignature: msg.new[sessionID]!.lastSignature,
|
lastSignature: msg.new[sessionID]!.lastSignature,
|
||||||
|
bytesSinceLastSignature: newBytesSinceLastSignature,
|
||||||
};
|
};
|
||||||
|
|
||||||
const sessionRowID = (await promised(
|
const sessionRowID = (await promised(
|
||||||
@@ -350,8 +480,18 @@ export class IDBStorage {
|
|||||||
)
|
)
|
||||||
)) as number;
|
)) as number;
|
||||||
|
|
||||||
|
if (shouldWriteSignature) {
|
||||||
|
await promised(
|
||||||
|
signatureAfter.put({
|
||||||
|
ses: sessionRowID,
|
||||||
|
// TODO: newLastIdx is a misnomer, it's actually more like nextIdx or length
|
||||||
|
idx: newLastIdx - 1,
|
||||||
|
signature: msg.new[sessionID]!.lastSignature,
|
||||||
|
} satisfies SignatureAfterRow)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for (const newTransaction of actuallyNewTransactions) {
|
for (const newTransaction of actuallyNewTransactions) {
|
||||||
nextIdx++;
|
|
||||||
await promised(
|
await promised(
|
||||||
transactions.add({
|
transactions.add({
|
||||||
ses: sessionRowID,
|
ses: sessionRowID,
|
||||||
@@ -359,6 +499,7 @@ export class IDBStorage {
|
|||||||
tx: newTransaction,
|
tx: newTransaction,
|
||||||
} satisfies TransactionRow)
|
} satisfies TransactionRow)
|
||||||
);
|
);
|
||||||
|
nextIdx++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,9 +523,10 @@ export class IDBStorage {
|
|||||||
coValues: IDBObjectStore;
|
coValues: IDBObjectStore;
|
||||||
sessions: IDBObjectStore;
|
sessions: IDBObjectStore;
|
||||||
transactions: IDBObjectStore;
|
transactions: IDBObjectStore;
|
||||||
|
signatureAfter: IDBObjectStore;
|
||||||
} {
|
} {
|
||||||
const tx = this.db.transaction(
|
const tx = this.db.transaction(
|
||||||
["coValues", "sessions", "transactions"],
|
["coValues", "sessions", "transactions", "signatureAfter"],
|
||||||
mode
|
mode
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -401,8 +543,9 @@ export class IDBStorage {
|
|||||||
const coValues = tx.objectStore("coValues");
|
const coValues = tx.objectStore("coValues");
|
||||||
const sessions = tx.objectStore("sessions");
|
const sessions = tx.objectStore("sessions");
|
||||||
const transactions = tx.objectStore("transactions");
|
const transactions = tx.objectStore("transactions");
|
||||||
|
const signatureAfter = tx.objectStore("signatureAfter");
|
||||||
|
|
||||||
return { coValues, sessions, transactions };
|
return { coValues, sessions, transactions, signatureAfter };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
60
yarn.lock
60
yarn.lock
@@ -853,7 +853,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@noble/hashes" "1.3.1"
|
"@noble/hashes" "1.3.1"
|
||||||
|
|
||||||
"@noble/hashes@1.3.1", "@noble/hashes@^1.3.1":
|
"@noble/hashes@1.3.1":
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
|
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
|
||||||
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
|
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
|
||||||
@@ -1589,6 +1589,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812"
|
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812"
|
||||||
integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==
|
integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==
|
||||||
|
|
||||||
|
"@types/image-blob-reduce@^4.1.1":
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/image-blob-reduce/-/image-blob-reduce-4.1.1.tgz#3c04b47809fe5a69d652bebfc118cd74f65742bd"
|
||||||
|
integrity sha512-Oe2EPjW+iZSsXccxZPebqHqXAUaOLir3eQVqPx0ryXeJZdCZx+gYvWBZtqYEcluP6f3bll1m06ahT26bX0+LOg==
|
||||||
|
dependencies:
|
||||||
|
"@types/pica" "*"
|
||||||
|
|
||||||
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
|
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
|
||||||
@@ -1641,6 +1648,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
|
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
|
||||||
integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
|
integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
|
||||||
|
|
||||||
|
"@types/pica@*":
|
||||||
|
version "9.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/pica/-/pica-9.0.1.tgz#adbdfc1190bb33a9da68d1fe501c2483dae3b142"
|
||||||
|
integrity sha512-hTsYxcy0MqIOKzeALuh3zOHyozBlndxV/bX9X52GBFq2XUQchZF6T0vcRYeT5P1ggmswi2LlIwHAH+bKWxxalg==
|
||||||
|
|
||||||
"@types/prop-types@*":
|
"@types/prop-types@*":
|
||||||
version "15.7.5"
|
version "15.7.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||||
@@ -3745,10 +3757,6 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-sta
|
|||||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||||
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
||||||
|
|
||||||
"fast-json-stable-stringify@https://github.com/tirithen/fast-json-stable-stringify#7a3dcf2":
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://github.com/tirithen/fast-json-stable-stringify#7a3dcf2e086222fcee52d354d50a6a80dea97aed"
|
|
||||||
|
|
||||||
fast-levenshtein@^2.0.6:
|
fast-levenshtein@^2.0.6:
|
||||||
version "2.0.6"
|
version "2.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||||
@@ -4219,6 +4227,11 @@ globby@11.1.0, globby@^11.1.0:
|
|||||||
merge2 "^1.4.1"
|
merge2 "^1.4.1"
|
||||||
slash "^3.0.0"
|
slash "^3.0.0"
|
||||||
|
|
||||||
|
glur@^1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/glur/-/glur-1.1.2.tgz#f20ea36db103bfc292343921f1f91e83c3467689"
|
||||||
|
integrity sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA==
|
||||||
|
|
||||||
"got@^ 12.6.1":
|
"got@^ 12.6.1":
|
||||||
version "12.6.1"
|
version "12.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549"
|
resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549"
|
||||||
@@ -4307,6 +4320,11 @@ has@^1.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind "^1.1.1"
|
function-bind "^1.1.1"
|
||||||
|
|
||||||
|
hash-wasm@^4.9.0:
|
||||||
|
version "4.9.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/hash-wasm/-/hash-wasm-4.9.0.tgz#7e9dcc9f7d6bd0cc802f2a58f24edce999744206"
|
||||||
|
integrity sha512-7SW7ejyfnRxuOc7ptQHSf4LDoZaWOivfzqw+5rpcQku0nHfmicPKE51ra9BiRLAmT8+gGLestr1XroUkqdjL6w==
|
||||||
|
|
||||||
hdr-histogram-js@^2.0.1:
|
hdr-histogram-js@^2.0.1:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz#0b860534655722b6e3f3e7dca7b78867cf43dcb5"
|
resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz#0b860534655722b6e3f3e7dca7b78867cf43dcb5"
|
||||||
@@ -4448,6 +4466,13 @@ ignore@^5.0.4, ignore@^5.2.0, ignore@^5.2.4:
|
|||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
|
||||||
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
|
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
|
||||||
|
|
||||||
|
image-blob-reduce@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/image-blob-reduce/-/image-blob-reduce-4.1.0.tgz#45f1e146ceaa45079025febe307f9b1e8b6833c9"
|
||||||
|
integrity sha512-iljleP8Fr7tS1ezrAazWi30abNPYXtBGXb9R9oTZDWObqiKq18AQJGTUb0wkBOtdCZ36/IirkuuAIIHTjBJIjA==
|
||||||
|
dependencies:
|
||||||
|
pica "^9.0.0"
|
||||||
|
|
||||||
import-fresh@^3.2.1:
|
import-fresh@^3.2.1:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||||
@@ -5917,6 +5942,14 @@ multimatch@5.0.0:
|
|||||||
arrify "^2.0.1"
|
arrify "^2.0.1"
|
||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
|
|
||||||
|
multimath@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/multimath/-/multimath-2.0.0.tgz#0d37acf67c328f30e3d8c6b0d3209e6082710302"
|
||||||
|
integrity sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g==
|
||||||
|
dependencies:
|
||||||
|
glur "^1.1.2"
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
|
||||||
mute-stream@0.0.8:
|
mute-stream@0.0.8:
|
||||||
version "0.0.8"
|
version "0.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
|
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
|
||||||
@@ -6262,7 +6295,7 @@ nx@16.6.0, "nx@>=16.5.1 < 17":
|
|||||||
"@nx/nx-win32-arm64-msvc" "16.6.0"
|
"@nx/nx-win32-arm64-msvc" "16.6.0"
|
||||||
"@nx/nx-win32-x64-msvc" "16.6.0"
|
"@nx/nx-win32-x64-msvc" "16.6.0"
|
||||||
|
|
||||||
object-assign@^4.0.1:
|
object-assign@^4.0.1, object-assign@^4.1.1:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||||
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
||||||
@@ -6614,6 +6647,16 @@ pend@~1.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
|
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
|
||||||
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
|
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
|
||||||
|
|
||||||
|
pica@^9.0.0:
|
||||||
|
version "9.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/pica/-/pica-9.0.1.tgz#9ba5a5e81fc09dca9800abef9fb8388434b18b2f"
|
||||||
|
integrity sha512-v0U4vY6Z3ztz9b4jBIhCD3WYoecGXCQeCsYep+sXRefViL+mVVoTL+wqzdPeE+GpBFsRUtQZb6dltvAt2UkMtQ==
|
||||||
|
dependencies:
|
||||||
|
glur "^1.1.2"
|
||||||
|
multimath "^2.0.0"
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
webworkify "^1.5.0"
|
||||||
|
|
||||||
picocolors@^1.0.0:
|
picocolors@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||||
@@ -8286,6 +8329,11 @@ webidl-conversions@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||||
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||||
|
|
||||||
|
webworkify@^1.5.0:
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/webworkify/-/webworkify-1.5.0.tgz#734ad87a774de6ebdd546e1d3e027da5b8f4a42c"
|
||||||
|
integrity sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g==
|
||||||
|
|
||||||
whatwg-url@^5.0.0:
|
whatwg-url@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||||
|
|||||||
Reference in New Issue
Block a user