From 47bd462079df3ab10c70f2b7f12fc94bd3b8258d Mon Sep 17 00:00:00 2001 From: Alex Currie-Clark Date: Tue, 1 Sep 2026 15:39:55 +0100 Subject: [PATCH] coln-repo --- packages/coln-repo/.gitignore | 4 + packages/coln-repo/README.md | 77 ++ packages/coln-repo/package.json | 40 + packages/coln-repo/playwright.config.ts | 42 + packages/coln-repo/pnpm-lock.yaml | 1028 +++++++++++++++++ packages/coln-repo/pnpm-workspace.yaml | 7 + packages/coln-repo/src/colnDocType.ts | 129 +++ packages/coln-repo/src/colnHandle.ts | 82 ++ packages/coln-repo/src/index.ts | 19 + packages/coln-repo/tests/browser/index.html | 11 + packages/coln-repo/tests/browser/main.ts | 107 ++ .../coln-repo/tests/browser/subduction.d.ts | 12 + packages/coln-repo/tests/fixtures/itemFfi.ts | 36 + packages/coln-repo/tests/fixtures/schema.ts | 24 + packages/coln-repo/tests/generated/Graph.ts | 15 + .../coln-repo/tests/generated/GraphRealm.json | 1 + .../tests/generated/GraphRealm.pretty | 13 + .../coln-repo/tests/generated/GraphRealm.ts | 39 + packages/coln-repo/tests/handle.test.ts | 165 +++ .../tests/helpers/TestNetworkAdapter.ts | 50 + .../coln-repo/tests/helpers/TestRepoPair.ts | 83 ++ packages/coln-repo/tests/package/consumer.ts | 73 ++ packages/coln-repo/tests/server.mjs | 114 ++ packages/coln-repo/tests/sync.spec.ts | 190 +++ packages/coln-repo/tsconfig.build.json | 12 + packages/coln-repo/tsconfig.json | 13 + packages/coln-repo/tsconfig.package.json | 8 + packages/coln-repo/vite.config.ts | 12 + 28 files changed, 2406 insertions(+) create mode 100644 packages/coln-repo/.gitignore create mode 100644 packages/coln-repo/README.md create mode 100644 packages/coln-repo/package.json create mode 100644 packages/coln-repo/playwright.config.ts create mode 100644 packages/coln-repo/pnpm-lock.yaml create mode 100644 packages/coln-repo/pnpm-workspace.yaml create mode 100644 packages/coln-repo/src/colnDocType.ts create mode 100644 packages/coln-repo/src/colnHandle.ts create mode 100644 packages/coln-repo/src/index.ts create mode 100644 packages/coln-repo/tests/browser/index.html create mode 100644 packages/coln-repo/tests/browser/main.ts create mode 100644 packages/coln-repo/tests/browser/subduction.d.ts create mode 100644 packages/coln-repo/tests/fixtures/itemFfi.ts create mode 100644 packages/coln-repo/tests/fixtures/schema.ts create mode 100644 packages/coln-repo/tests/generated/Graph.ts create mode 100644 packages/coln-repo/tests/generated/GraphRealm.json create mode 100644 packages/coln-repo/tests/generated/GraphRealm.pretty create mode 100644 packages/coln-repo/tests/generated/GraphRealm.ts create mode 100644 packages/coln-repo/tests/handle.test.ts create mode 100644 packages/coln-repo/tests/helpers/TestNetworkAdapter.ts create mode 100644 packages/coln-repo/tests/helpers/TestRepoPair.ts create mode 100644 packages/coln-repo/tests/package/consumer.ts create mode 100644 packages/coln-repo/tests/server.mjs create mode 100644 packages/coln-repo/tests/sync.spec.ts create mode 100644 packages/coln-repo/tsconfig.build.json create mode 100644 packages/coln-repo/tsconfig.json create mode 100644 packages/coln-repo/tsconfig.package.json create mode 100644 packages/coln-repo/vite.config.ts diff --git a/packages/coln-repo/.gitignore b/packages/coln-repo/.gitignore new file mode 100644 index 00000000..fe30a0cf --- /dev/null +++ b/packages/coln-repo/.gitignore @@ -0,0 +1,4 @@ +dist/ +playwright-report/ +test-results/ +tests/browser/dist/ diff --git a/packages/coln-repo/README.md b/packages/coln-repo/README.md new file mode 100644 index 00000000..f680cc4a --- /dev/null +++ b/packages/coln-repo/README.md @@ -0,0 +1,77 @@ +# `@coln-project/repo` + +Coln Repo exposes Coln stores as [Automerge Repo](https://github.com/automerge/automerge-repo) document handles. Use them to create or find stores, read the current typed view, make atomic changes, and subscribe to changes to update application state or UI. Automerge Repo provides persistence and synchronization. + +Coln Repo currently requires the experimental [doctypes](https://github.com/automerge/automerge-repo/tree/doctypes) branch. + +## Create + +Create a Coln store using generated bindings. + +```ts +import * as Bindings from "./ExampleRealm.js"; +import { create } from "@coln-project/repo"; + +const handle = create(repo, Bindings); +``` + +## Find + +Find a Coln store by Automerge URL. Bindings are optional and must match the document schema when supplied. + +```ts +import * as Bindings from "./ExampleRealm.js"; +import { find } from "@coln-project/repo"; + +const handle = await find(repo, url, Bindings); +``` + +Apply bindings to a raw handle later if needed: + +```ts +import * as Bindings from "./ExampleRealm.js"; +import { applyBindings, find } from "@coln-project/repo"; + +const rawHandle = await find(repo, url); +const handle = applyBindings(rawHandle, Bindings); +``` + +> [!IMPORTANT] +> Once bindings are applied, later raw lookups in the same Repo may return that bound handle. It retains the raw Coln operations and adds `root` to documents and change transactions. + +## Read + +Read the current document synchronously or subscribe to changes. + +```ts +const doc = handle.doc(); +doc.scanTable("Example.Items"); +doc.root.Items.values(); + +handle.on("change", ({ doc }) => { + doc?.scanTable("Example.Items"); + doc?.root.Items.values(); +}); +``` + +Documents expose `heads()`, `jsonIR()`, `rowById()`, and `scanTable()`. + +## Write + +Changes are synchronous and atomic. A thrown error aborts the transaction. + +```ts +handle.change((tx) => { + tx.root.Items.add(); +}); +``` + +Transactions expose `add(path, values)`; bound transactions also expose `root`. + +The underlying `StoreHandle` is available through `handle.fullDoc().store` for advanced use. Its lifecycle operations bypass Coln Repo changes, events, and synchronization; prefer `doc()` and `change()`. + +## Current Limitations + +Values returned by `handle.doc()` are current documents, not durable snapshots. + +Generated read methods such as `has()` and `values()` are not supported inside `handle.change()`. Typed transactions currently support writes only. diff --git a/packages/coln-repo/package.json b/packages/coln-repo/package.json new file mode 100644 index 00000000..ad226573 --- /dev/null +++ b/packages/coln-repo/package.json @@ -0,0 +1,40 @@ +{ + "name": "@coln-project/repo", + "version": "0.0.1-alpha.0", + "private": true, + "license": "(Apache-2.0 OR MIT)", + "files": [ + "dist" + ], + "type": "module", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "pnpm run typecheck && pnpm run test:unit && pnpm run test:e2e", + "test:unit": "tsx --test \"tests/**/*.test.ts\"", + "test:package": "pnpm run build && tsc -p tsconfig.package.json && node --input-type=module --eval \"const pkg = await import('@coln-project/repo'); if (typeof pkg.find !== 'function') throw new Error('missing find export')\"", + "test:e2e": "pnpm run test:package && playwright test" + }, + "dependencies": { + "@automerge/automerge-repo": "github:automerge/automerge-repo#doctypes&path:packages/automerge-repo", + "@coln-project/runtime": "link:../coln-js-runtime" + }, + "devDependencies": { + "@automerge/automerge-subduction": "0.16.1", + "@playwright/test": "^1.62.1", + "@types/node": "^25.9.5", + "@types/ws": "^8.18.1", + "tsx": "^4.23.13", + "typescript": "^6.0.3", + "vite": "^8.2.2", + "vite-plugin-wasm": "^3.6.0", + "ws": "^8.21.3" + } +} diff --git a/packages/coln-repo/playwright.config.ts b/packages/coln-repo/playwright.config.ts new file mode 100644 index 00000000..9a741d42 --- /dev/null +++ b/packages/coln-repo/playwright.config.ts @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { defineConfig, devices } from "@playwright/test" + +const webPort = Number(process.env.COLN_REPO_TEST_WEB_PORT ?? 5174) + +export default defineConfig({ + testDir: "./tests", + testMatch: "**/*.spec.ts", + timeout: 30_000, + expect: { timeout: 10_000 }, + fullyParallel: false, + retries: 0, + use: { + baseURL: `http://127.0.0.1:${webPort}`, + trace: "retain-on-failure", + }, + webServer: [ + { + command: "node tests/server.mjs", + port: 3031, + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + }, + { + command: `./node_modules/.bin/vite --host 127.0.0.1 --port ${webPort}`, + url: `http://127.0.0.1:${webPort}`, + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + }, + ], + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}) diff --git a/packages/coln-repo/pnpm-lock.yaml b/packages/coln-repo/pnpm-lock.yaml new file mode 100644 index 00000000..41883aa0 --- /dev/null +++ b/packages/coln-repo/pnpm-lock.yaml @@ -0,0 +1,1028 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + +importers: + + .: + dependencies: + '@automerge/automerge-repo': + specifier: github:automerge/automerge-repo#doctypes&path:packages/automerge-repo + version: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo + '@coln-project/runtime': + specifier: link:../coln-js-runtime + version: link:../coln-js-runtime + devDependencies: + '@automerge/automerge-subduction': + specifier: 0.16.1 + version: 0.16.1 + '@playwright/test': + specifier: ^1.62.1 + version: 1.62.1 + '@types/node': + specifier: ^25.9.5 + version: 25.9.5 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + tsx: + specifier: ^4.23.13 + version: 4.23.13 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite: + specifier: ^8.2.2 + version: 8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(tsx@4.23.13) + vite-plugin-wasm: + specifier: ^3.6.0 + version: 3.6.0(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(tsx@4.23.13)) + ws: + specifier: ^8.21.3 + version: 8.21.3 + +packages: + + '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': + resolution: {gitHosted: true, integrity: sha512-9LCmV33S9KLTv/4mlMaRxJanF7Trh/ltok1o499dJanBJUt4AVpj1c+ZJlP4nm73/JeOsSUhW/rDr7mH393giA==, path: packages/automerge-repo, tarball: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5} + version: 2.6.0-subduction.47 + engines: {node: '>=22.13'} + + '@automerge/automerge-subduction@0.16.1': + resolution: {integrity: sha512-alH7U4eYn0O7sT6hNLv7CJhNODJi+QD6qfRQTNfZCxL3Q5JZdJ8lFpdvXUkBf+I3bu0GLvVlTaYPWGrBihjpIw==} + + '@automerge/automerge@3.3.2': + resolution: {integrity: sha512-9vCdCL7pdQwUra66SBxPVHr+/t9epXKni9KDeak2rNBFMzABVh2u6gSpcwxi3jkR5qr047jVqZv9DlrOfVxLFw==} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@oxc-project/types@0.147.0': + resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==} + + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} + hasBin: true + + '@rolldown/binding-android-arm-eabi@1.2.6': + resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.6': + resolution: {integrity: sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.6': + resolution: {integrity: sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.6': + resolution: {integrity: sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.6': + resolution: {integrity: sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': + resolution: {integrity: sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.6': + resolution: {integrity: sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.6': + resolution: {integrity: sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.6': + resolution: {integrity: sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.6': + resolution: {integrity: sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.6': + resolution: {integrity: sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.6': + resolution: {integrity: sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.6': + resolution: {integrity: sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.6': + resolution: {integrity: sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.6': + resolution: {integrity: sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@types/node@25.9.5': + resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} + hasBin: true + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + rolldown@1.2.6: + resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tsx@4.23.13: + resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + vite-plugin-wasm@3.6.0: + resolution: {integrity: sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.32.6: + resolution: {integrity: sha512-WfA8WNrh6r9osuGwVm+aIPM4jmMW2LKHV1Yv9thYpOtL4HVF/kobh95K/O8v2jDCpDmP2EoY+6uvuqHXCZ6ZLw==} + +snapshots: + + '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.32.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-subduction@0.16.1': {} + + '@automerge/automerge@3.3.2': {} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@noble/hashes@1.8.0': {} + + '@oxc-project/types@0.147.0': {} + + '@playwright/test@1.62.1': + dependencies: + playwright: 1.62.1 + + '@rolldown/binding-android-arm-eabi@1.2.6': + optional: true + + '@rolldown/binding-android-arm64@1.2.6': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.6': + optional: true + + '@rolldown/binding-darwin-x64@1.2.6': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.6': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.6': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.6': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.6': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.6': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.6': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@types/node@25.9.5': + dependencies: + undici-types: 7.24.6 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.9.5 + + base-x@5.0.1: {} + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.6: + optionalDependencies: + cbor-extract: 2.2.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-libc@2.1.2: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + eventemitter3@5.0.4: {} + + fast-sha256@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + isomorphic-ws@5.0.0(ws@8.21.3): + dependencies: + ws: 8.21.3 + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + playwright-core@1.62.1: {} + + playwright@1.62.1: + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rolldown@1.2.6: + dependencies: + '@oxc-project/types': 0.147.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.6 + '@rolldown/binding-android-arm64': 1.2.6 + '@rolldown/binding-darwin-arm64': 1.2.6 + '@rolldown/binding-darwin-x64': 1.2.6 + '@rolldown/binding-freebsd-x64': 1.2.6 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.6 + '@rolldown/binding-linux-arm64-gnu': 1.2.6 + '@rolldown/binding-linux-arm64-musl': 1.2.6 + '@rolldown/binding-linux-ppc64-gnu': 1.2.6 + '@rolldown/binding-linux-s390x-gnu': 1.2.6 + '@rolldown/binding-linux-x64-gnu': 1.2.6 + '@rolldown/binding-linux-x64-musl': 1.2.6 + '@rolldown/binding-openharmony-arm64': 1.2.6 + '@rolldown/binding-win32-arm64-msvc': 1.2.6 + '@rolldown/binding-win32-x64-msvc': 1.2.6 + + source-map-js@1.2.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tsx@4.23.13: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + typescript@6.0.3: {} + + undici-types@7.24.6: {} + + uuid@14.0.2: {} + + vite-plugin-wasm@3.6.0(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(tsx@4.23.13)): + dependencies: + vite: 8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(tsx@4.23.13) + + vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(tsx@4.23.13): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.26 + rolldown: 1.2.6 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 25.9.5 + esbuild: 0.28.2 + fsevents: 2.3.3 + tsx: 4.23.13 + + ws@8.21.3: {} + + xstate@5.32.6: {} diff --git a/packages/coln-repo/pnpm-workspace.yaml b/packages/coln-repo/pnpm-workspace.yaml new file mode 100644 index 00000000..087ce062 --- /dev/null +++ b/packages/coln-repo/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +allowBuilds: + cbor-extract: false + '@automerge/automerge-repo@git+https://github.com/automerge/automerge-repo.git': true + esbuild: true +overrides: + "@automerge/automerge": 3.3.2 + "@automerge/automerge-subduction": 0.16.1 diff --git a/packages/coln-repo/src/colnDocType.ts b/packages/coln-repo/src/colnDocType.ts new file mode 100644 index 00000000..45ca8d73 --- /dev/null +++ b/packages/coln-repo/src/colnDocType.ts @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + StoreHandle, + type CommitChunk, + type ColnSchema, + type RealmBindings, + type TransactionHandle, +} from "@coln-project/runtime" +import { defineDocumentType, type SedimentreeMeta } from "@automerge/automerge-repo/slim" + +export type { ColnSchema, RealmBindings } from "@coln-project/runtime" + +export type ColnState = { + store: StoreHandle + bindings?: RealmBindings +} + +type ColnDocumentBase = Pick +type ColnTransactionBase = Pick + +export type ColnDocument = + Bindings extends RealmBindings + ? ColnDocumentBase & Pick, "root"> + : ColnDocumentBase + +export type ColnTransaction = + Bindings extends RealmBindings + ? ColnTransactionBase & Pick, "root"> + : ColnTransactionBase + +export type ColnChange = ( + transaction: ColnTransaction, +) => void + +export const colnDocType = defineDocumentType< + ColnState, + ColnDocumentBase, + ColnChange, + ColnSchema +>({ + name: "coln", + empty: () => ({ store: StoreHandle.empty() }), + init: schema => ({ store: StoreHandle.fromTheory(serializeSchema(schema)) }), + view: createDocument, + change: (state, change) => runTransaction(state, change), + heads: state => state.store.heads(), + hasData: state => state.store.heads().length > 0, + sedimentree: { + metadata: state => commitChunks(state).map(commitMetadata), + materialize: (state, metadata) => { + const wantedHeads = new Set(metadata.map(entry => entry.head)) + // TODO: Add a runtime commitChunksByHash API if full-history scans become costly. + return commitChunks(state) + .filter(chunk => wantedHeads.has(chunk.hash)) + .map(chunk => ({ ...commitMetadata(chunk), bytes: new Uint8Array(chunk.bytes) })) + }, + apply: (state, blobs) => { + if (blobs.length > 0) { + // TODO: "Add independent StoreHandle snapshots for Repo document states": + // apply blobs to a snapshot and return a new document. + state.store.applyChunkBytes(blobs) + } + return state + }, + liveHashes: state => commitChunks(state).map(chunk => chunk.hash), + }, +}) + +function createDocument(state: ColnState): ColnDocumentBase { + const { store, bindings } = state + const document: ColnDocumentBase & { root?: unknown } = { + heads: () => store.heads(), + jsonIR: () => store.jsonIR(), + rowById: (path, rowId) => store.rowById(path, rowId), + scanTable: path => store.scanTable(path), + } + if (bindings) document.root = new bindings.View(store).root + return Object.freeze(document) +} + +function createTransaction( + state: ColnState, + transaction: TransactionHandle, +): ColnTransactionBase { + const { store, bindings } = state + const exposed: ColnTransactionBase & { root?: unknown } = { + add: (path, values) => transaction.add(path, values), + } + if (bindings) exposed.root = new bindings.Transaction(store, transaction).root + return Object.freeze(exposed) +} + +function runTransaction(document: ColnState, change: ColnChange): ColnState { + // TODO: "Add independent StoreHandle snapshots for Repo document states": + // start the transaction from a snapshot and leave this document unchanged. + const { store, bindings } = document + const transaction = store.beginTransaction() + try { + change(createTransaction(document, transaction)) + const commit = transaction.commit() + return { store: commit.takeStore(), bindings } + } catch (error) { + // beginTransaction() moved ownership of the store into the transaction, and on + // error we cannot return a fresh document. Recover the (aborted) store back onto + // the existing state object, otherwise it would hold a dead StoreHandle. + document.store = transaction.takeStore() + throw error + } +} + +function serializeSchema(schema: ColnSchema): string { + const serialized = JSON.stringify(schema) + if (serialized === undefined) throw new Error("creating a Coln store requires a schema") + return serialized +} + +// TODO: Every call pays to serialize chunk bytes as number[] across the wasm +// boundary, even when only hashes/parents are needed (metadata, liveHashes). +// Switch to Uint8Array output once CommitChunk uses serde_bytes in the runtime. +function commitChunks(document: ColnState): CommitChunk[] { + return document.store.commitChunksAfter([]) +} + +function commitMetadata(chunk: CommitChunk): SedimentreeMeta { + return { kind: "commit", head: chunk.hash, parents: chunk.parents } +} diff --git a/packages/coln-repo/src/colnHandle.ts b/packages/coln-repo/src/colnHandle.ts new file mode 100644 index 00000000..3a892b9a --- /dev/null +++ b/packages/coln-repo/src/colnHandle.ts @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { + AutomergeUrl, + CrdtDocHandle, + DocumentType, + Repo, +} from "@automerge/automerge-repo"; +import type { RealmBindings } from "@coln-project/runtime"; +import { + colnDocType, + type ColnChange, + type ColnDocument, + type ColnSchema, + type ColnState, +} from "./colnDocType.js"; + +type FoundColnHandle = + Bindings extends RealmBindings ? ColnHandle : ColnHandle; + +export type ColnHandle = + CrdtDocHandle< + DocumentType, ColnChange, ColnSchema> + >; + +export function applyBindings( + handle: ColnHandle | ColnHandle, + bindings: Bindings, +): ColnHandle { + const state = handle.fullDoc(); + const existingBindings = state.bindings; + + if (existingBindings) { + if (existingBindings !== bindings) { + throw new TypeError("Coln handle already uses different realm bindings"); + } + return handle as unknown as ColnHandle; + } + + const actualSchema = canonicalJson(JSON.parse(handle.doc().jsonIR())); + const bindingsSchema = canonicalJson(bindings.schema); + if (actualSchema !== bindingsSchema) { + throw new TypeError("Realm bindings schema does not match Coln document schema"); + } + + state.bindings = bindings; + + return handle as unknown as ColnHandle; +} + +export function create( + repo: Repo, + bindings: Bindings, +): ColnHandle { + const handle = repo.create(bindings.schema, colnDocType); + + return applyBindings(handle, bindings); +} + +export async function find( + repo: Repo, + url: AutomergeUrl, + // a rest tuple (rather than an optional parameter) keeps `undefined` in the + // inferred Bindings, so maybe-undefined arguments produce a union return type + ...rest: [bindings: Bindings] | [] +): Promise> { + const [bindings] = rest; + const handle = await repo.find(url, colnDocType); + + return (bindings === undefined ? handle : applyBindings(handle, bindings)) as FoundColnHandle; +} + +function canonicalJson(value: unknown): string | undefined { + return JSON.stringify(value, (_key, current: unknown) => { + if (current === null || Array.isArray(current) || typeof current !== "object") return current; + return Object.fromEntries( + Object.entries(current).sort(([left], [right]) => left.localeCompare(right)), + ); + }); +} diff --git a/packages/coln-repo/src/index.ts b/packages/coln-repo/src/index.ts new file mode 100644 index 00000000..8afb07f1 --- /dev/null +++ b/packages/coln-repo/src/index.ts @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export { + colnDocType, + type ColnChange, + type ColnDocument, + type RealmBindings, + type ColnSchema, + type ColnTransaction, +} from "./colnDocType.js" + +export { + applyBindings, + create, + find, + type ColnHandle, +} from "./colnHandle.js" diff --git a/packages/coln-repo/tests/browser/index.html b/packages/coln-repo/tests/browser/index.html new file mode 100644 index 00000000..37bb5c8a --- /dev/null +++ b/packages/coln-repo/tests/browser/index.html @@ -0,0 +1,11 @@ + + + + + + Coln Repo Test + + + + + diff --git a/packages/coln-repo/tests/browser/main.ts b/packages/coln-repo/tests/browser/main.ts new file mode 100644 index 00000000..6c1619dc --- /dev/null +++ b/packages/coln-repo/tests/browser/main.ts @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + Repo, + type AutomergeUrl, + type CrdtDocHandle, +} from "@automerge/automerge-repo" +import { initSync } from "@automerge/automerge-subduction/slim" +import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64" +import type { RowView, Value } from "@coln-project/runtime" +import { + colnDocType, + applyBindings, + type ColnHandle, + type ColnSchema, +} from "../../src/index.js" +import * as itemFfi from "../fixtures/itemFfi" + +initSync({ module: Uint8Array.from(atob(wasmBase64), char => char.charCodeAt(0)) }) + +type RawHandle = CrdtDocHandle +type ItemHandle = ColnHandle + +const repo = new Repo({ + subductionWebsocketEndpoints: ["ws://127.0.0.1:3031"], +}) +let handle: RawHandle | undefined +let typedHandle: ItemHandle | undefined + +const api = { + create(schema: ColnSchema): AutomergeUrl { + handle = repo.create(schema, colnDocType) + return handle.url + }, + + async find(url: AutomergeUrl): Promise { + handle = await repo.find(url, colnDocType) + }, + + createTyped(): AutomergeUrl { + const rawHandle = repo.create(itemFfi.schema, colnDocType) + handle = rawHandle + typedHandle = applyBindings(rawHandle, itemFfi) + return typedHandle.url + }, + + applyTypedBindings(): void { + typedHandle = applyBindings(currentHandle(), itemFfi) + }, + + add(path: string, values: Value[]): void { + currentHandle().change(transaction => transaction.add(path, values)) + }, + + rows(path: string): RowView[] { + return currentHandle().doc().scanTable(path) + }, + + addTyped(value: string): void { + currentTypedHandle().change(transaction => { + transaction.root.Items(stringValue(value)).add() + }) + }, + + typedCount(value: string): number { + const rows = currentTypedHandle().doc().root.Items(stringValue(value)).values() + let count = 0 + for (let next = rows.next(); !next.done; next = rows.next()) count += 1 + return count + }, + + heads(): string[] { + return currentHandle().doc().heads() + }, + + async flush(): Promise { + await repo.flush() + }, + + async shutdown(): Promise { + await repo.shutdown() + }, +} + +function currentHandle(): RawHandle { + if (!handle) throw new Error("no Coln store is open") + return handle +} + +function currentTypedHandle(): ItemHandle { + if (!typedHandle) throw new Error("no typed Coln store is open") + return typedHandle +} + +function stringValue(value: string): Value { + return { tag: "string", value } +} + +declare global { + interface Window { + colnTest: typeof api + } +} + +window.colnTest = api diff --git a/packages/coln-repo/tests/browser/subduction.d.ts b/packages/coln-repo/tests/browser/subduction.d.ts new file mode 100644 index 00000000..ee91318e --- /dev/null +++ b/packages/coln-repo/tests/browser/subduction.d.ts @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +declare module "@automerge/automerge-subduction/slim" { + export * from "@automerge/automerge-subduction" + export function initSync(input: { module: Uint8Array }): void +} + +declare module "@automerge/automerge-subduction/wasm-base64" { + export const wasmBase64: string +} diff --git a/packages/coln-repo/tests/fixtures/itemFfi.ts b/packages/coln-repo/tests/fixtures/itemFfi.ts new file mode 100644 index 00000000..f181ec17 --- /dev/null +++ b/packages/coln-repo/tests/fixtures/itemFfi.ts @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + RowIdSet, + type StoreHandle, + type TransactionHandle, + type Value, +} from "@coln-project/runtime" +export { itemSchema as schema } from "./schema" + +interface Root { + Items: (value: Value) => T +} + +export class View { + root: Root + + constructor(store: StoreHandle) { + this.root = { + Items: value => new RowIdSet.View(store, "Test.Items", [value]), + } + } +} + +export class Transaction extends View { + declare root: Root + + constructor(store: StoreHandle, transaction: TransactionHandle) { + super(store) + this.root = { + Items: value => new RowIdSet.Transaction(store, "Test.Items", [value], transaction), + } + } +} diff --git a/packages/coln-repo/tests/fixtures/schema.ts b/packages/coln-repo/tests/fixtures/schema.ts new file mode 100644 index 00000000..d8a45f9d --- /dev/null +++ b/packages/coln-repo/tests/fixtures/schema.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnSchema } from "../../src/index.js" + +export const itemSchema = { + entities: [ + { + path: [["Test"], ["Items"]], + value: { + entityVariant: { tag: "table" }, + columns: [ + { + path: [["value"]], + type: { tag: "builtin", type: "builtinString" }, + }, + ], + primaryKey: null, + }, + }, + ], + rules: [], +} satisfies ColnSchema diff --git a/packages/coln-repo/tests/generated/Graph.ts b/packages/coln-repo/tests/generated/Graph.ts new file mode 100644 index 00000000..db000cd9 --- /dev/null +++ b/packages/coln-repo/tests/generated/Graph.ts @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import * as runtime from "@coln-project/runtime"; + +export interface View { + V: runtime.ColnSet.View; + E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.View; +} + +export interface Transaction extends View { + V: runtime.ColnSet.Transaction; + E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.Transaction; +} \ No newline at end of file diff --git a/packages/coln-repo/tests/generated/GraphRealm.json b/packages/coln-repo/tests/generated/GraphRealm.json new file mode 100644 index 00000000..b83d89f3 --- /dev/null +++ b/packages/coln-repo/tests/generated/GraphRealm.json @@ -0,0 +1 @@ +{"entities":[{"path":[["GraphRealm"],["V"]],"value":{"entityVariant":{"tag":"table"},"columns":[],"primaryKey":null}},{"path":[["GraphRealm"],["E"]],"value":{"entityVariant":{"tag":"table"},"columns":[{"path":[["a"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}},{"path":[["b"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}}],"primaryKey":null}}],"rules":[{"path":[["GraphRealm"],["V"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[],"varTypes":[],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":null,"values":[]}}],"consequents":[]}},{"path":[["GraphRealm"],["E"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[[["a"]],[["b"]]],"varTypes":[{"tag":"rowId","path":[["GraphRealm"],["V"]]},{"tag":"rowId","path":[["GraphRealm"],["V"]]}],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["E"]],"rowId":null,"values":[{"column":0,"term":{"tag":"var","index":0}},{"column":1,"term":{"tag":"var","index":1}}]}}],"consequents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":0},"values":[]}},{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":1},"values":[]}}]}}]} \ No newline at end of file diff --git a/packages/coln-repo/tests/generated/GraphRealm.pretty b/packages/coln-repo/tests/generated/GraphRealm.pretty new file mode 100644 index 00000000..b4a7d502 --- /dev/null +++ b/packages/coln-repo/tests/generated/GraphRealm.pretty @@ -0,0 +1,13 @@ +flatrealm + entities + table ℜ.V := [] + table ℜ.E := [.a : ℜ.V, .b : ℜ.V] + end + rules + enforced ℜ.V.foreignKey := ℜ.V [] ⊢ ⊤ + enforced ℜ.E.foreignKey a b := ℜ.E [ + .a ↦ a, + .b ↦ b + ] ⊢ a ∈ ℜ.V [] ∧ b ∈ ℜ.V [] + end +end \ No newline at end of file diff --git a/packages/coln-repo/tests/generated/GraphRealm.ts b/packages/coln-repo/tests/generated/GraphRealm.ts new file mode 100644 index 00000000..b7ca28db --- /dev/null +++ b/packages/coln-repo/tests/generated/GraphRealm.ts @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import schema from "./GraphRealm.json" with { type: "json" } +export { schema } +import * as runtime from "@coln-project/runtime" +import * as Graph from "./Graph" + +export class View { + root: Graph.View + + constructor(store: runtime.StoreHandle) { + this.root = { + V: new runtime.RowIdSet.View(store, "GraphRealm.V", []), + E: (a: runtime.Value) => { + return (b: runtime.Value) => { + return new runtime.RowIdSet.View(store, "GraphRealm.E", [a, b]) + } + }, + } + } +} + +export class Transaction extends View { + root: Graph.Transaction + + constructor(store: runtime.StoreHandle, transaction: runtime.TransactionHandle) { + super(store) + this.root = { + V: new runtime.RowIdSet.Transaction(store, "GraphRealm.V", [], transaction), + E: (a: runtime.Value) => { + return (b: runtime.Value) => { + return new runtime.RowIdSet.Transaction(store, "GraphRealm.E", [a, b], transaction) + } + }, + } + } +} diff --git a/packages/coln-repo/tests/handle.test.ts b/packages/coln-repo/tests/handle.test.ts new file mode 100644 index 00000000..a8096050 --- /dev/null +++ b/packages/coln-repo/tests/handle.test.ts @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import assert from "node:assert/strict"; +import { afterEach, beforeEach, test } from "node:test"; +import type { Value } from "@coln-project/runtime"; +import { applyBindings, colnDocType, create, find, type ColnDocument } from "../src/index"; +import * as itemFfi from "./fixtures/itemFfi"; +import schema from "./generated/GraphRealm.json" with { type: "json" }; +import * as GraphRealm from "./generated/GraphRealm"; +import { createTestRepoPair, waitForChange, type TestRepoPair } from "./helpers/TestRepoPair"; + +let repos: TestRepoPair; + +beforeEach(async () => { + repos = await createTestRepoPair(); +}); + +afterEach(async () => { + await repos.shutdown(); +}); + +test("create returns a bound, initialized handle", () => { + const handle = create(repos.source, GraphRealm); + const document = handle.doc(); + + assert.deepEqual(Object.keys(document).sort(), ["heads", "jsonIR", "root", "rowById", "scanTable"]); + assert.deepEqual(JSON.parse(document.jsonIR()), schema); + assert.deepEqual(document.heads(), handle.rawHeads()); + assert.deepEqual(document.scanTable("GraphRealm.V"), []); +}); + +test("document can be updated and changes are reflected", () => { + const handle = create(repos.source, GraphRealm); + + let v1!: Value, v2!: Value, e1!: Value; + + handle.change((tx) => { + v1 = tx.root.V.add(); + v2 = tx.root.V.add(); + e1 = tx.root.E(v1)(v2).add(); + }); + + const doc = handle.doc(); + + assert(doc.root.V.has(v1)); + assert(doc.root.V.has(v2)); + assert(doc.root.E(v1)(v2).has(e1)); + assert.equal(v1.tag, "row_id"); + assert.deepEqual(doc.rowById("GraphRealm.V", v1.value)?.rowId, v1); +}); + +test("changes expose only safe Coln operations", () => { + const handle = create(repos.source, GraphRealm); + + handle.change(transaction => { + assert.deepEqual(Object.keys(transaction).sort(), ["add", "root"]); + transaction.add("GraphRealm.V", []); + }); + + assert.equal(handle.doc().scanTable("GraphRealm.V").length, 1); +}); + +test("failed changes are aborted and leave the handle usable", () => { + const handle = create(repos.source, GraphRealm); + let committed!: Value; + + assert.throws( + () => + handle.change((transaction) => { + transaction.root.V.add(); + throw new Error("abort"); + }), + new Error("abort"), + ); + handle.change((transaction) => { + committed = transaction.root.V.add(); + }); + + assert.deepEqual( + handle + .doc() + .scanTable("GraphRealm.V") + .map((row) => row.rowId), + [committed], + ); +}); + +test("typed handles sync changes", async () => { + const handle = create(repos.source, GraphRealm); + const replica = await find(repos.replica, handle.url, GraphRealm); + let vertex!: Value; + + const changed = new Promise>(resolve => { + replica.once("change", payload => { + if (payload.doc) resolve(payload.doc); + }); + }); + handle.change((tx) => { + vertex = tx.root.V.add(); + }); + const changedDocument = await changed; + + assert(changedDocument.root.V.has(vertex)); + assert.deepEqual(changedDocument.scanTable("GraphRealm.V").map(row => row.rowId), [vertex]); + assert(replica.doc().root.V.has(vertex)); +}); + +test("raw changes sync into a typed handle", async () => { + const handle = create(repos.source, GraphRealm); + const replica = await repos.replica.find(handle.url, colnDocType); + let vertex!: Value; + + const changed = waitForChange(handle); + replica.change((transaction) => { + vertex = transaction.add("GraphRealm.V", []); + }); + await changed; + + assert(handle.doc().root.V.has(vertex)); +}); + +test("find returns a raw handle when bindings are omitted", async () => { + const handle = create(repos.source, GraphRealm); + const replica = await find(repos.replica, handle.url); + + assert.deepEqual(Object.keys(replica.doc()).sort(), ["heads", "jsonIR", "rowById", "scanTable"]); +}); + +test("bindings can be applied to a raw found handle later", async () => { + const handle = create(repos.source, GraphRealm); + const rawReplica = await find(repos.replica, handle.url); + const replica = applyBindings(rawReplica, GraphRealm); + + assert.strictEqual(replica, rawReplica); + assert.strictEqual(applyBindings(replica, GraphRealm), replica); + + let vertex!: Value; + replica.change(transaction => { + vertex = transaction.root.V.add(); + }); + + assert(replica.doc().root.V.has(vertex)); + assert.deepEqual(replica.doc().scanTable("GraphRealm.V").map(row => row.rowId), [vertex]); +}); + +test("applying different bindings to a bound handle is rejected", () => { + const handle = create(repos.source, GraphRealm); + const otherBindings = { ...GraphRealm }; + + assert.throws( + () => applyBindings(handle, otherBindings), + new TypeError("Coln handle already uses different realm bindings"), + ); +}); + +test("applying bindings for a different schema is rejected", () => { + const handle = repos.source.create(schema, colnDocType); + + assert.throws( + () => applyBindings(handle, itemFfi), + new TypeError("Realm bindings schema does not match Coln document schema"), + ); +}); diff --git a/packages/coln-repo/tests/helpers/TestNetworkAdapter.ts b/packages/coln-repo/tests/helpers/TestNetworkAdapter.ts new file mode 100644 index 00000000..e25621d3 --- /dev/null +++ b/packages/coln-repo/tests/helpers/TestNetworkAdapter.ts @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + NetworkAdapter, + type Message, + type PeerId, +} from "@automerge/automerge-repo" + +export class TestNetworkAdapter extends NetworkAdapter { + #connected = false + #peer?: TestNetworkAdapter + + static createConnectedPair(): [TestNetworkAdapter, TestNetworkAdapter] { + const first = new TestNetworkAdapter() + const second = new TestNetworkAdapter() + first.#peer = second + second.#peer = first + return [first, second] + } + + isReady(): boolean { + return true + } + + async whenReady(): Promise {} + + connect(peerId: PeerId): void { + this.peerId = peerId + this.#connected = true + } + + disconnect(): void { + this.#connected = false + } + + announce(peerId: PeerId): void { + this.emit("peer-candidate", { peerId, peerMetadata: {} }) + } + + send(message: Message): void { + if (!this.#connected) return + queueMicrotask(() => this.#peer?.receive(message)) + } + + private receive(message: Message): void { + if (this.#connected) this.emit("message", message) + } +} diff --git a/packages/coln-repo/tests/helpers/TestRepoPair.ts b/packages/coln-repo/tests/helpers/TestRepoPair.ts new file mode 100644 index 00000000..707f79d2 --- /dev/null +++ b/packages/coln-repo/tests/helpers/TestRepoPair.ts @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + initSubduction, + Repo, +} from "@automerge/automerge-repo" +import { setTimeout as delay } from "node:timers/promises" +import { TestNetworkAdapter } from "./TestNetworkAdapter" + +export interface TestRepoPair { + source: Repo + replica: Repo + shutdown(): Promise +} + +export async function createTestRepoPair(): Promise { + await initSubduction() + const [sourceAdapter, replicaAdapter] = TestNetworkAdapter.createConnectedPair() + const source = new Repo({ + subductionAdapters: [{ adapter: sourceAdapter, serviceName: "test", role: "connect" }], + subductionTimeouts: { healMaxAttempts: 0, syncMs: 1_000 }, + }) + const replica = new Repo({ + subductionAdapters: [ + { adapter: replicaAdapter, serviceName: "test", role: "accept" }, + ], + subductionTimeouts: { healMaxAttempts: 0, syncMs: 1_000 }, + }) + + const sourceConnected = waitForConnection(source) + const replicaConnected = waitForConnection(replica) + sourceAdapter.announce(replica.peerId) + replicaAdapter.announce(source.peerId) + await Promise.all([sourceConnected, replicaConnected]) + + return { + source, + replica, + async shutdown() { + await Promise.all([source.flush(), replica.flush()]) + await Promise.all([ + source.subduction.then(subduction => subduction.disconnectAll()), + replica.subduction.then(subduction => subduction.disconnectAll()), + ]) + await Promise.all([source.shutdown(), replica.shutdown()]) + }, + } +} + +export function waitForChange(handle: { + once(event: "change", listener: () => void): unknown +}): Promise { + return withTimeout( + new Promise(resolve => handle.once("change", () => resolve())), + "handle change", + ) +} + +function waitForConnection(repo: Repo): Promise { + if (repo.isSubductionConnected()) return Promise.resolve() + return withTimeout( + new Promise(resolve => { + const onConnection = ({ connected }: { connected: boolean }) => { + if (!connected) return + repo.off("subduction-connection", onConnection) + resolve() + } + repo.on("subduction-connection", onConnection) + }), + "Subduction connection", + ) +} + +function withTimeout(promise: Promise, operation: string): Promise { + return Promise.race([ + promise, + delay(5_000, undefined, { ref: false }).then(() => { + throw new Error(`${operation} timed out`) + }), + ]) +} diff --git a/packages/coln-repo/tests/package/consumer.ts b/packages/coln-repo/tests/package/consumer.ts new file mode 100644 index 00000000..e7d36bb0 --- /dev/null +++ b/packages/coln-repo/tests/package/consumer.ts @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { AutomergeUrl, Repo } from "@automerge/automerge-repo" +import type { Value } from "@coln-project/runtime" +import { + colnDocType, + find, + type ColnChange, + type ColnDocument, + type ColnHandle, + type ColnTransaction, + type RealmBindings, +} from "@coln-project/repo" + +type Equal = + (() => Type extends Left ? 1 : 2) extends () => Type extends Right ? 1 : 2 + ? true + : false +type Expect = Type + +declare const repo: Repo +declare const url: AutomergeUrl +declare const bindings: RealmBindings +declare const optionalBindings: RealmBindings | undefined + +const raw = find(repo, url) +const explicitUndefined = find(repo, url, undefined) +const typed = find(repo, url, bindings) +const optional = find(repo, url, optionalBindings) + +void raw.then(handle => { + handle.doc().scanTable("Example.Items") + handle.fullDoc().store.heads() +}) + +void typed.then(handle => { + handle.doc().root + handle.on("change", payload => { + payload.doc?.root + payload.handle.doc().root + }) + handle.addListener("change", payload => payload.doc?.root) + handle.listeners("change").forEach(listener => listener) + handle.on("heads-changed", payload => { + payload.doc.store + payload.handle.doc().root + }) + handle.change(transaction => { + const value: Value = transaction.add("Example.Items", []) + value + transaction.root + }) +}) + +type DocumentOperation = "heads" | "jsonIR" | "rowById" | "scanTable" + +export type TypeChecks = [ + Expect>>, + Expect>>, + Expect>>>, + Expect>>>, + Expect["doc"]>, ColnDocument>>, + Expect< + Equal["change"]>[0], ColnChange> + >, + Expect>[0], ColnTransaction>>, + Expect>, + Expect, DocumentOperation | "root">>, + Expect>, + Expect, "add" | "root">>, +] diff --git a/packages/coln-repo/tests/server.mjs b/packages/coln-repo/tests/server.mjs new file mode 100644 index 00000000..d91e2fc5 --- /dev/null +++ b/packages/coln-repo/tests/server.mjs @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { WebSocketServer } from "ws" +import { + MemorySigner, + MemoryStorage, + Subduction, +} from "@automerge/automerge-subduction" + +class WebSocketTransport { + #socket + #queue = [] + #waiters = [] + #errorWaiters = [] + #closed = false + #closedPromise + #resolveClosed + #disconnectCallback = null + + constructor(socket) { + this.#socket = socket + this.#closedPromise = new Promise(resolve => { + this.#resolveClosed = resolve + }) + + socket.on("message", data => { + const bytes = toUint8Array(data) + const waiter = this.#waiters.shift() + if (waiter) { + this.#errorWaiters.shift() + waiter(bytes) + } else { + this.#queue.push(bytes) + } + }) + socket.on("close", () => this.#markClosed(new Error("WebSocket closed"))) + socket.on("error", error => this.#markClosed(error)) + } + + onDisconnect(callback) { + this.#disconnectCallback = callback + } + + async sendBytes(bytes) { + if (this.#closed) throw new Error("WebSocket closed") + await new Promise((resolve, reject) => { + this.#socket.send(Buffer.from(bytes), error => (error ? reject(error) : resolve())) + }) + } + + recvBytes() { + const queued = this.#queue.shift() + if (queued) return Promise.resolve(queued) + if (this.#closed) return Promise.reject(new Error("WebSocket closed")) + return new Promise((resolve, reject) => { + this.#waiters.push(resolve) + this.#errorWaiters.push(reject) + }) + } + + async disconnect() { + this.#markClosed(new Error("WebSocket closed"), false) + this.#socket.close() + } + + closed() { + return this.#closedPromise + } + + #markClosed(error, notify = true) { + if (this.#closed) return + this.#closed = true + this.#resolveClosed() + for (const reject of this.#errorWaiters) reject(error) + this.#waiters = [] + this.#errorWaiters = [] + if (notify) this.#disconnectCallback?.() + } +} + +function toUint8Array(data) { + if (data instanceof Uint8Array) { + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) + } + if (data instanceof ArrayBuffer) return new Uint8Array(data) + if (Array.isArray(data)) return toUint8Array(Buffer.concat(data)) + return new Uint8Array(Buffer.from(data)) +} + +const port = 3031 +const serviceName = `127.0.0.1:${port}` +const subduction = new Subduction({ + signer: new MemorySigner(), + storage: new MemoryStorage(), +}) +const server = new WebSocketServer({ port }) + +server.on("connection", socket => { + subduction.acceptTransport(new WebSocketTransport(socket), serviceName).catch(error => { + if (!String(error?.message ?? error).includes("closed")) console.error(error) + }) +}) + +async function shutdown() { + await subduction.disconnectAll().catch(() => {}) + server.close(() => process.exit(0)) +} + +process.on("SIGINT", shutdown) +process.on("SIGTERM", shutdown) + +console.log(`Subduction test relay listening on ws://127.0.0.1:${port}`) diff --git a/packages/coln-repo/tests/sync.spec.ts b/packages/coln-repo/tests/sync.spec.ts new file mode 100644 index 00000000..c4a154e5 --- /dev/null +++ b/packages/coln-repo/tests/sync.spec.ts @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { expect, test, type Page } from "@playwright/test" +import { + initSubduction, + Repo, + type AutomergeUrl, + type CrdtDocHandle, +} from "@automerge/automerge-repo" +import { colnDocType } from "../src/index.js" +import { itemSchema } from "./fixtures/schema" + +const table = "Test.Items" +type RawHandle = CrdtDocHandle + +test("browser and Node repos create, find, and update each other's stores", async ({ browser }) => { + await initSubduction() + const context = await browser.newContext() + const pages: Page[] = [] + let nodeRepo: Repo | undefined + + try { + nodeRepo = new Repo({ + subductionWebsocketEndpoints: ["ws://127.0.0.1:3031"], + }) + const browserCreator = await openPage(context.newPage()) + pages.push(browserCreator) + const browserUrl = await browserCreator.evaluate( + schema => window.colnTest.create(schema), + itemSchema, + ) + await add(browserCreator, "browser-one") + await browserCreator.evaluate(() => window.colnTest.flush()) + + const nodeFinder = await nodeRepo.find(browserUrl, colnDocType) + await expectNodeRows(nodeFinder, ["browser-one"]) + + addInNode(nodeFinder, "node-two") + await nodeRepo.flush() + await expectRows(browserCreator, ["browser-one", "node-two"]) + await expectSameHeads(browserCreator, nodeFinder) + + const nodeCreator = nodeRepo.create(itemSchema, colnDocType) + addInNode(nodeCreator, "node-one") + await nodeRepo.flush() + + const browserFinder = await openPage(context.newPage()) + pages.push(browserFinder) + await browserFinder.evaluate( + url => window.colnTest.find(url), + nodeCreator.url as AutomergeUrl, + ) + await expectRows(browserFinder, ["node-one"]) + + await add(browserFinder, "browser-two") + await browserFinder.evaluate(() => window.colnTest.flush()) + await expectNodeRows(nodeCreator, ["browser-two", "node-one"]) + await expectSameHeads(browserFinder, nodeCreator) + } finally { + try { + await shutdownPages(pages) + } finally { + try { + await nodeRepo?.shutdown() + } finally { + await context.close() + } + } + } +}) + +test("reloads without schema and applies typed bindings", async ({ browser }) => { + const context = await browser.newContext() + const pages: Page[] = [] + + try { + const creator = await openPage(context.newPage()) + pages.push(creator) + const url = await creator.evaluate(() => window.colnTest.createTyped()) + + await creator.evaluate(() => window.colnTest.addTyped("one")) + await creator.evaluate(() => window.colnTest.flush()) + + const finder = await openPage(context.newPage()) + pages.push(finder) + await finder.evaluate(url => window.colnTest.find(url), url) + await expectRows(finder, ["one"]) + await finder.evaluate(() => window.colnTest.applyTypedBindings()) + await expectTypedCount(finder, "one", 1) + + await finder.evaluate(() => window.colnTest.addTyped("two")) + await finder.evaluate(() => window.colnTest.flush()) + await expectTypedCount(creator, "two", 1) + + await shutdownPages([creator, finder]) + await creator.close() + await finder.close() + + const reloaded = await openPage(context.newPage()) + pages.push(reloaded) + await reloaded.evaluate(url => window.colnTest.find(url), url) + await expectRows(reloaded, ["one", "two"]) + await reloaded.evaluate(() => window.colnTest.applyTypedBindings()) + await expectTypedCount(reloaded, "one", 1) + await expectTypedCount(reloaded, "two", 1) + } finally { + try { + await shutdownPages(pages) + } finally { + await context.close() + } + } +}) + +async function openPage(pagePromise: Promise): Promise { + const page = await pagePromise + await page.goto("/") + await page.waitForFunction(() => window.colnTest !== undefined) + return page +} + +async function shutdownPages(pages: Page[]): Promise { + await Promise.all( + pages + .filter(page => !page.isClosed()) + .map(page => page.evaluate(() => window.colnTest.shutdown()).catch(() => undefined)), + ) +} + +async function add(page: Page, value: string) { + await page.evaluate( + ({ table, value }) => window.colnTest.add(table, [{ tag: "string", value }]), + { table, value }, + ) +} + +async function expectRows(page: Page, values: string[]) { + await expect + .poll(() => + page.evaluate( + table => + window.colnTest + .rows(table) + .map(row => row.values[0]) + .filter(value => value?.tag === "string") + .map(value => value.value) + .sort(), + table, + ), + ) + .toEqual([...values].sort()) +} + +function addInNode(handle: RawHandle, value: string) { + handle.change(transaction => transaction.add(table, [{ tag: "string", value }])) +} + +async function expectNodeRows(handle: RawHandle, values: string[]) { + await expect + .poll(() => + handle + .doc() + .scanTable(table) + .map(row => row.values[0]) + .filter(value => value?.tag === "string") + .map(value => value.value) + .sort(), + ) + .toEqual([...values].sort()) +} + +async function expectSameHeads(page: Page, handle: RawHandle) { + await expect + .poll(async () => { + const browserHeads = (await page.evaluate(() => window.colnTest.heads())).sort() + const nodeHeads = handle.doc().heads().sort() + return browserHeads.join(",") === nodeHeads.join(",") + ? "equal" + : `browser: ${browserHeads.join(",")}; Node: ${nodeHeads.join(",")}` + }) + .toBe("equal") +} + +async function expectTypedCount(page: Page, value: string, count: number) { + await expect + .poll(() => page.evaluate(value => window.colnTest.typedCount(value), value)) + .toBe(count) +} diff --git a/packages/coln-repo/tsconfig.build.json b/packages/coln-repo/tsconfig.build.json new file mode 100644 index 00000000..274a9026 --- /dev/null +++ b/packages/coln-repo/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["tests"] +} diff --git a/packages/coln-repo/tsconfig.json b/packages/coln-repo/tsconfig.json new file mode 100644 index 00000000..6d028484 --- /dev/null +++ b/packages/coln-repo/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "ES2022"], + "strict": true, + "noEmit": true, + "types": ["node", "vite/client"] + }, + "include": ["src/**/*.ts", "tests/**/*.ts", "*.config.ts"], + "exclude": ["tests/package"] +} diff --git a/packages/coln-repo/tsconfig.package.json b/packages/coln-repo/tsconfig.package.json new file mode 100644 index 00000000..d8dc1cd9 --- /dev/null +++ b/packages/coln-repo/tsconfig.package.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [] + }, + "include": ["tests/package/**/*.ts"], + "exclude": [] +} diff --git a/packages/coln-repo/vite.config.ts b/packages/coln-repo/vite.config.ts new file mode 100644 index 00000000..06488873 --- /dev/null +++ b/packages/coln-repo/vite.config.ts @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { defineConfig } from "vite" +import wasm from "vite-plugin-wasm" + +export default defineConfig({ + root: "tests/browser", + plugins: [wasm()], + build: { target: "esnext" }, +})