From 145f48020096beb117e3efc2345b8d399ced659c Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:19:29 +0200 Subject: [PATCH 01/18] INIT RxDB-tanstack-db-plugin --- packages/rxdb-db-collection/CHANGELOG.md | 3 + packages/rxdb-db-collection/package.json | 71 +++++++ packages/rxdb-db-collection/src/index.ts | 1 + packages/rxdb-db-collection/src/rxdb.ts | 184 ++++++++++++++++++ .../rxdb-db-collection/tests/rxdb.test.ts | 117 +++++++++++ .../rxdb-db-collection/tsconfig.docs.json | 9 + packages/rxdb-db-collection/tsconfig.json | 20 ++ packages/rxdb-db-collection/vite.config.ts | 21 ++ 8 files changed, 426 insertions(+) create mode 100644 packages/rxdb-db-collection/CHANGELOG.md create mode 100644 packages/rxdb-db-collection/package.json create mode 100644 packages/rxdb-db-collection/src/index.ts create mode 100644 packages/rxdb-db-collection/src/rxdb.ts create mode 100644 packages/rxdb-db-collection/tests/rxdb.test.ts create mode 100644 packages/rxdb-db-collection/tsconfig.docs.json create mode 100644 packages/rxdb-db-collection/tsconfig.json create mode 100644 packages/rxdb-db-collection/vite.config.ts diff --git a/packages/rxdb-db-collection/CHANGELOG.md b/packages/rxdb-db-collection/CHANGELOG.md new file mode 100644 index 000000000..62f848a93 --- /dev/null +++ b/packages/rxdb-db-collection/CHANGELOG.md @@ -0,0 +1,3 @@ +# @tanstack/electric-db-collection + +## 0.0.0 diff --git a/packages/rxdb-db-collection/package.json b/packages/rxdb-db-collection/package.json new file mode 100644 index 000000000..a2a2606c5 --- /dev/null +++ b/packages/rxdb-db-collection/package.json @@ -0,0 +1,71 @@ +{ + "name": "@tanstack/rxdb-db-collection", + "description": "RxDB collection for TanStack DB", + "version": "0.1.4", + "dependencies": { + "rxdb": "16.17.2", + "@standard-schema/spec": "^1.0.0", + "@tanstack/db": "workspace:*", + "@tanstack/store": "^0.7.0", + "debug": "^4.4.1" + }, + "devDependencies": { + "@types/debug": "^4.1.12", + "@vitest/coverage-istanbul": "^3.0.9" + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "src" + ], + "main": "dist/cjs/index.cjs", + "module": "dist/esm/index.js", + "packageManager": "pnpm@10.6.3", + "peerDependencies": { + "rxdb": ">=16.17.2", + "typescript": ">=4.7" + }, + "author": "Kyle Mathews", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/db.git", + "directory": "packages/rxdb-db-collection" + }, + "homepage": "https://tanstack.com/db", + "keywords": [ + "rxdb", + "nosql", + "realtime", + "local-first", + "sync-engine", + "sync", + "replication", + "opfs", + "indexeddb", + "localstorage", + "optimistic", + "typescript" + ], + "scripts": { + "build": "vite build", + "dev": "vite build --watch", + "lint": "eslint . --fix", + "test": "npx vitest --run" + }, + "sideEffects": false, + "type": "module", + "types": "dist/esm/index.d.ts" +} diff --git a/packages/rxdb-db-collection/src/index.ts b/packages/rxdb-db-collection/src/index.ts new file mode 100644 index 000000000..93145f7d1 --- /dev/null +++ b/packages/rxdb-db-collection/src/index.ts @@ -0,0 +1 @@ +export * from "./rxdb" diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts new file mode 100644 index 000000000..9e6a389ec --- /dev/null +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -0,0 +1,184 @@ +import { RxCollection, RxDocument, RxQuery, getFromMapOrCreate, lastOfArray } from "rxdb/plugins/core" + + +import { Store } from "@tanstack/store" +import DebugModule from "debug" +import type { + CollectionConfig, + DeleteMutationFnParams, + InsertMutationFnParams, + ResolveType, + SyncConfig, + UpdateMutationFnParams, + UtilsRecord, +} from "@tanstack/db" +import type { StandardSchemaV1 } from "@standard-schema/spec" +import { REPLICATION_STATE_BY_COLLECTION } from 'rxdb/plugins/replication' + +const debug = DebugModule.debug(`ts/db:electric`) + + +export type RxDBCollectionConfig< + TExplicit extends object = object, + TSchema extends StandardSchemaV1 = never, + /** + * By definition, RxDB primary keys + * must be a string + */ + TKey extends string = string, +> = Omit< + CollectionConfig, + "insert" | "update" | "delete" | "getKey" | "sync" +> & { + rxCollection: RxCollection +}; + +/** + * Creates RxDB collection options for use with a standard Collection + * + * @template TExplicit - The explicit type of items in the collection (highest priority) + * @template TSchema - The schema type for validation and type inference (second priority) + * @template TFallback - The fallback type if no explicit or schema type is provided + * @param config - Configuration options for the Electric collection + * @returns Collection options with utilities + */ +export function rxdbCollectionOptions< + TExplicit extends object = object, + TSchema extends StandardSchemaV1 = never, + TKey extends string = string +>( + config: RxDBCollectionConfig +) { + const { ...restConfig } = config + const rxCollection = config.rxCollection + debug("wrapping RxDB collection", name) + + + // "getKey" + const primaryPath = rxCollection.schema.primaryPath + const getKey: CollectionConfig>[`getKey`] = (item) => { + const key: string = (item as any)[primaryPath] + return key + } + + /** + * "sync" + * Notice that this describes the Sync between the local RxDB collection + * and the in-memory tanstack-db collection. + * It is not about sync between a client and a server! + */ + type SyncParams = Parameters[`sync`]>[0] + const sync = { + sync: (params: SyncParams) => { + const { begin, write, commit, markReady } = params + + async function initialFetch() { + /** + * RxDB stores a last-write-time + * which can be used to "sort" document writes, + * so for initial sync we iterate over that. + */ + let cursor: RxDocument | undefined = undefined + const syncBatchSize = 1000 // make this configureable + let done = false + begin() + + while (!done) { + let query: RxQuery[], unknown, unknown> + if (cursor) { + query = rxCollection.find({ + selector: { + $or: [ + { '_meta.lwt': { $gt: cursor._data._meta.lwt } }, + { + '_meta.lwt': cursor._data._meta.lwt, + [primaryPath]: { + $gt: cursor.primary + }, + } + ] + }, + sort: [ + { '_meta.lwt': 'asc' }, + { [primaryPath]: 'asc' } + ], + limit: syncBatchSize + }) + } else { + query = rxCollection.find({ + selector: {}, + sort: [ + { '_meta.lwt': 'asc' }, + { [primaryPath]: 'asc' } + ], + limit: syncBatchSize + }) + } + + const docs = await query.exec(); + cursor = lastOfArray(docs) + if(docs.length === 0){ + done = true + break; + } + + docs.forEach(d => { + write({ + type: 'insert', + value: d.toMutableJSON() + }) + }) + + } + + commit() + } + + + async function start(){ + await initialFetch(); + + markReady() + } + + + start() + }, + // Expose the getSyncMetadata function + getSyncMetadata: undefined, + } + + const collectionConfig: CollectionConfig> = { + ...restConfig, + getKey, + sync, + onInsert: async (params: InsertMutationFnParams) => { + debug("insert", params) + return rxCollection.insert(params.value) + }, + update: async (params: UpdateMutationFnParams) => { + debug("update", params.id, params.value) + const doc = await rxCollection.findOne(params.id).exec() + if (doc) { + await doc.patch(params.value) + } + }, + delete: async (params: DeleteMutationFnParams) => { + debug("delete", params.id) + const doc = await rxCollection.findOne(params.id).exec() + if (doc) { + await doc.remove() + } + }, + // Optional: hook RxDB’s observable into TanStack/DB reactivity + utils: { + subscribe(listener) { + const sub = rxCollection.$.subscribe(ev => { + listener({ op: ev.operation, doc: ev.documentData }) + }) + return () => sub.unsubscribe() + }, + }, + } + return collectionConfig; +} diff --git a/packages/rxdb-db-collection/tests/rxdb.test.ts b/packages/rxdb-db-collection/tests/rxdb.test.ts new file mode 100644 index 000000000..79fd43bf4 --- /dev/null +++ b/packages/rxdb-db-collection/tests/rxdb.test.ts @@ -0,0 +1,117 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" +import { + CollectionImpl, + createCollection, + createTransaction, +} from "@tanstack/db" +import { rxdbCollectionOptions } from "../src/rxdb" +import type { + Collection, + InsertMutationFnParams, + MutationFnParams, + PendingMutation, + Transaction, + TransactionWithMutations, + UtilsRecord, +} from "@tanstack/db" +import type { StandardSchemaV1 } from "@standard-schema/spec" +import { RxCollection, RxDatabase, createRxDatabase } from 'rxdb/plugins/core' +import { getRxStorageMemory } from 'rxdb/plugins/storage-memory' + +// Mock the ShapeStream module +const mockSubscribe = vi.fn() +const mockStream = { + subscribe: mockSubscribe, +} + +type TestDocType = { + id: string + name: string +} + +// Helper to advance timers and allow microtasks to flush +const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0)) + +describe(`RxDB Integration`, () => { + let collection: Collection< + any, + string | number, + UtilsRecord, + StandardSchemaV1, + any + >; + type RxCollections = { test: RxCollection }; + let db: RxDatabase; + + beforeEach(async () => { + if (db) { + await db.remove(); + } + db = await createRxDatabase({ + name: 'my-rxdb', + storage: getRxStorageMemory() + }); + const collections = await db.addCollections({ + test: { + schema: { + version: 0, + type: 'object', + primaryKey: 'id', + properties: { + id: { + type: 'string', + maxLength: 100 + }, + name: { + type: 'string' + } + } + } + } + }); + const rxCollection: RxCollection = collections.test; + + const options = rxdbCollectionOptions({ + rxCollection: rxCollection + }) + + collection = createCollection(options) + }); + + + + it(`should initialize and fetch initial data`, async () => { + const queryKey = [`testItems`] + const initialItems: Array = [ + { id: `1`, name: `Item 1` }, + { id: `2`, name: `Item 2` }, + ] + + const queryFn = vi.fn().mockResolvedValue(initialItems) + + // Wait for the query to complete and collection to update + await vi.waitFor( + () => { + expect(queryFn).toHaveBeenCalledTimes(1) + expect(collection.size).toBeGreaterThan(0) + }, + { + timeout: 1000, // Give it a reasonable timeout + interval: 50, // Check frequently + } + ) + + // Additional wait for internal processing if necessary + await flushPromises() + + // Verify the collection state contains our items + expect(collection.size).toBe(initialItems.length) + expect(collection.get(`1`)).toEqual(initialItems[0]) + expect(collection.get(`2`)).toEqual(initialItems[1]) + + // Verify the synced data + expect(collection.syncedData.size).toBe(initialItems.length) + expect(collection.syncedData.get(`1`)).toEqual(initialItems[0]) + expect(collection.syncedData.get(`2`)).toEqual(initialItems[1]) + }) +}); diff --git a/packages/rxdb-db-collection/tsconfig.docs.json b/packages/rxdb-db-collection/tsconfig.docs.json new file mode 100644 index 000000000..5a73feb02 --- /dev/null +++ b/packages/rxdb-db-collection/tsconfig.docs.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "paths": { + "@tanstack/db": ["../db/src"] + } + }, + "include": ["src"] +} diff --git a/packages/rxdb-db-collection/tsconfig.json b/packages/rxdb-db-collection/tsconfig.json new file mode 100644 index 000000000..7e586bab3 --- /dev/null +++ b/packages/rxdb-db-collection/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", + "declaration": true, + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "paths": { + "@tanstack/store": ["../store/src"] + } + }, + "include": ["src", "tests", "vite.config.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/rxdb-db-collection/vite.config.ts b/packages/rxdb-db-collection/vite.config.ts new file mode 100644 index 000000000..c7968f28a --- /dev/null +++ b/packages/rxdb-db-collection/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig, mergeConfig } from "vitest/config" +import { tanstackViteConfig } from "@tanstack/config/vite" +import packageJson from "./package.json" + +const config = defineConfig({ + test: { + name: packageJson.name, + dir: `./tests`, + environment: `jsdom`, + coverage: { enabled: true, provider: `istanbul`, include: [`src/**/*`] }, + typecheck: { enabled: true }, + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: `./src/index.ts`, + srcDir: `./src`, + }) +) From e90663d83e67ac1eb06615a2af3fb6fdd4743abb Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:31:33 +0200 Subject: [PATCH 02/18] ADD more tests --- packages/rxdb-db-collection/src/rxdb.ts | 111 ++++++++++++------ .../rxdb-db-collection/tests/rxdb.test.ts | 91 +++++++++----- 2 files changed, 138 insertions(+), 64 deletions(-) diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index 9e6a389ec..874d7b2d7 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -1,5 +1,11 @@ -import { RxCollection, RxDocument, RxQuery, getFromMapOrCreate, lastOfArray } from "rxdb/plugins/core" - +import { + RxCollection, + RxDocument, + RxQuery, + getFromMapOrCreate, + lastOfArray +} from "rxdb/plugins/core" +import type { Subscription } from 'rxjs' import { Store } from "@tanstack/store" import DebugModule from "debug" @@ -13,21 +19,19 @@ import type { UtilsRecord, } from "@tanstack/db" import type { StandardSchemaV1 } from "@standard-schema/spec" -import { REPLICATION_STATE_BY_COLLECTION } from 'rxdb/plugins/replication' const debug = DebugModule.debug(`ts/db:electric`) export type RxDBCollectionConfig< TExplicit extends object = object, - TSchema extends StandardSchemaV1 = never, - /** - * By definition, RxDB primary keys - * must be a string - */ - TKey extends string = string, + TSchema extends StandardSchemaV1 = never > = Omit< - CollectionConfig, + CollectionConfig< + TExplicit, + string, // because RxDB primary keys must be strings + TSchema + >, "insert" | "update" | "delete" | "getKey" | "sync" > & { rxCollection: RxCollection @@ -44,11 +48,13 @@ export type RxDBCollectionConfig< */ export function rxdbCollectionOptions< TExplicit extends object = object, - TSchema extends StandardSchemaV1 = never, - TKey extends string = string + TSchema extends StandardSchemaV1 = never >( - config: RxDBCollectionConfig + config: RxDBCollectionConfig ) { + type Row = ResolveType; + type Key = string; // because RxDB primary keys must be strings + const { ...restConfig } = config const rxCollection = config.rxCollection debug("wrapping RxDB collection", name) @@ -56,7 +62,7 @@ export function rxdbCollectionOptions< // "getKey" const primaryPath = rxCollection.schema.primaryPath - const getKey: CollectionConfig>[`getKey`] = (item) => { + const getKey: CollectionConfig['getKey'] = (item) => { const key: string = (item as any)[primaryPath] return key } @@ -67,12 +73,15 @@ export function rxdbCollectionOptions< * and the in-memory tanstack-db collection. * It is not about sync between a client and a server! */ - type SyncParams = Parameters[`sync`]>[0] - const sync = { + type SyncParams = Parameters['sync']>[0] + const sync: SyncConfig = { sync: (params: SyncParams) => { + console.log('SYCN START!!!') const { begin, write, commit, markReady } = params + let ready = false async function initialFetch() { + console.log('initialFetch() START'); /** * RxDB stores a last-write-time * which can be used to "sort" document writes, @@ -80,10 +89,10 @@ export function rxdbCollectionOptions< */ let cursor: RxDocument | undefined = undefined const syncBatchSize = 1000 // make this configureable - let done = false begin() - while (!done) { + while (!ready) { + console.log('initialFetch() loooooop'); let query: RxQuery[], unknown, unknown> if (cursor) { query = rxCollection.find({ @@ -116,9 +125,10 @@ export function rxdbCollectionOptions< } const docs = await query.exec(); + console.dir(docs.map(d => d.toJSON())) cursor = lastOfArray(docs) - if(docs.length === 0){ - done = true + if (docs.length === 0) { + ready = true break; } @@ -130,19 +140,61 @@ export function rxdbCollectionOptions< }) } + console.log('initialFetch() DONE'); commit() } + type WriteMessage = Parameters[0] + const buffer: WriteMessage[] = [] + const queue = (msg: WriteMessage) => { + if (!ready) { + buffer.push(msg) + return + } + begin() + write(msg as any) + commit() + } - async function start(){ + let sub: Subscription + async function startOngoingFetch() { + // Subscribe early and buffer live changes during initial load and ongoing + sub = rxCollection.$.subscribe((ev) => { + const cur = ev.documentData as Row + switch (ev.operation) { + case 'INSERT': + if (cur) queue({ type: 'insert', value: cur }) + break + case 'UPDATE': + if (cur) queue({ type: 'update', value: cur }) + break + case 'DELETE': + queue({ type: 'delete', value: cur }) + break + } + }) + } + + + async function start() { + startOngoingFetch() await initialFetch(); + if (buffer.length) { + begin() + for (const msg of buffer) write(msg as any) + commit() + buffer.length = 0 + } + markReady() } start() + + return () => sub.unsubscribe() }, // Expose the getSyncMetadata function getSyncMetadata: undefined, @@ -152,33 +204,24 @@ export function rxdbCollectionOptions< ...restConfig, getKey, sync, - onInsert: async (params: InsertMutationFnParams) => { + onInsert: async (params) => { debug("insert", params) return rxCollection.insert(params.value) }, - update: async (params: UpdateMutationFnParams) => { + onUpdate: async (params) => { debug("update", params.id, params.value) const doc = await rxCollection.findOne(params.id).exec() if (doc) { await doc.patch(params.value) } }, - delete: async (params: DeleteMutationFnParams) => { + onDelete: async (params) => { debug("delete", params.id) const doc = await rxCollection.findOne(params.id).exec() if (doc) { await doc.remove() } - }, - // Optional: hook RxDB’s observable into TanStack/DB reactivity - utils: { - subscribe(listener) { - const sub = rxCollection.$.subscribe(ev => { - listener({ op: ev.operation, doc: ev.documentData }) - }) - return () => sub.unsubscribe() - }, - }, + } } return collectionConfig; } diff --git a/packages/rxdb-db-collection/tests/rxdb.test.ts b/packages/rxdb-db-collection/tests/rxdb.test.ts index 79fd43bf4..b65946427 100644 --- a/packages/rxdb-db-collection/tests/rxdb.test.ts +++ b/packages/rxdb-db-collection/tests/rxdb.test.ts @@ -4,7 +4,7 @@ import { createCollection, createTransaction, } from "@tanstack/db" -import { rxdbCollectionOptions } from "../src/rxdb" +import { RxDBCollectionConfig, rxdbCollectionOptions } from "../src/rxdb" import type { Collection, InsertMutationFnParams, @@ -15,8 +15,10 @@ import type { UtilsRecord, } from "@tanstack/db" import type { StandardSchemaV1 } from "@standard-schema/spec" -import { RxCollection, RxDatabase, createRxDatabase } from 'rxdb/plugins/core' +import { RxCollection, addRxPlugin, createRxDatabase } from 'rxdb/plugins/core' +import { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode' import { getRxStorageMemory } from 'rxdb/plugins/storage-memory' +import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv' // Mock the ShapeStream module const mockSubscribe = vi.fn() @@ -28,11 +30,13 @@ type TestDocType = { id: string name: string } +type RxCollections = { test: RxCollection }; // Helper to advance timers and allow microtasks to flush const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0)) describe(`RxDB Integration`, () => { + addRxPlugin(RxDBDevModePlugin) let collection: Collection< any, string | number, @@ -40,16 +44,17 @@ describe(`RxDB Integration`, () => { StandardSchemaV1, any >; - type RxCollections = { test: RxCollection }; - let db: RxDatabase; - beforeEach(async () => { - if (db) { - await db.remove(); - } - db = await createRxDatabase({ - name: 'my-rxdb', - storage: getRxStorageMemory() + let dbNameId = 0; + async function createTestState( + initialDocs: TestDocType[] = [], + config: Partial> = {} + ) { + const db = await createRxDatabase({ + name: 'my-rxdb-' + (dbNameId++), + storage: wrappedValidateAjvStorage({ + storage: getRxStorageMemory() + }) }); const collections = await db.addCollections({ test: { @@ -70,39 +75,35 @@ describe(`RxDB Integration`, () => { } }); const rxCollection: RxCollection = collections.test; + if (initialDocs.length > 0) { + const insertResult = await rxCollection.bulkInsert(initialDocs) + expect(insertResult.error.length).toBe(0) + } const options = rxdbCollectionOptions({ - rxCollection: rxCollection + rxCollection: rxCollection, + startSync: true, + ...config }) collection = createCollection(options) - }); + await collection.stateWhenReady() + return { + collection, + rxCollection, + db + } + } it(`should initialize and fetch initial data`, async () => { - const queryKey = [`testItems`] const initialItems: Array = [ { id: `1`, name: `Item 1` }, { id: `2`, name: `Item 2` }, ] - const queryFn = vi.fn().mockResolvedValue(initialItems) - - // Wait for the query to complete and collection to update - await vi.waitFor( - () => { - expect(queryFn).toHaveBeenCalledTimes(1) - expect(collection.size).toBeGreaterThan(0) - }, - { - timeout: 1000, // Give it a reasonable timeout - interval: 50, // Check frequently - } - ) - - // Additional wait for internal processing if necessary - await flushPromises() + const { collection, db } = await createTestState(initialItems); // Verify the collection state contains our items expect(collection.size).toBe(initialItems.length) @@ -113,5 +114,35 @@ describe(`RxDB Integration`, () => { expect(collection.syncedData.size).toBe(initialItems.length) expect(collection.syncedData.get(`1`)).toEqual(initialItems[0]) expect(collection.syncedData.get(`2`)).toEqual(initialItems[1]) + + await db.remove() + }) + + it(`should update the collection when RxDB changes data`, async () => { + const initialItems: Array = [ + { id: `1`, name: `Item 1` }, + { id: `2`, name: `Item 2` }, + ] + + const { collection, rxCollection, db } = await createTestState(initialItems); + + + // inserts + const doc = await rxCollection.insert({ id: '3', name: 'inserted' }) + expect(collection.get(`3`).name).toEqual('inserted') + + // updates + await doc.getLatest().patch({ name: 'updated' }) + expect(collection.get(`3`).name).toEqual('updated') + + + // deletes + await doc.getLatest().remove() + console.log(':::::::::::::::::::::::::::::::::1') + console.dir(collection.get(`3`)) + expect(collection.get(`3`)).toEqual(undefined) + + + await db.remove() }) }); From 580b8184994aa21240dec6cce26be4f8a3991189 Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:01:51 +0200 Subject: [PATCH 03/18] ADD more tests --- packages/rxdb-db-collection/src/helper.ts | 17 +++++++ packages/rxdb-db-collection/src/rxdb.ts | 49 +++++++++++++------ .../rxdb-db-collection/tests/rxdb.test.ts | 42 +++++++++++++++- 3 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 packages/rxdb-db-collection/src/helper.ts diff --git a/packages/rxdb-db-collection/src/helper.ts b/packages/rxdb-db-collection/src/helper.ts new file mode 100644 index 000000000..86b39f4cb --- /dev/null +++ b/packages/rxdb-db-collection/src/helper.ts @@ -0,0 +1,17 @@ +const RESERVED_RXDB_FIELDS = new Set([ + '_rev', + '_deleted', + '_attachments', + '_meta', +]) + +export function stripRxdbFields>(obj: T): T { + if (!obj) return obj + const out: any = Array.isArray(obj) ? [] : {} + for (const k of Object.keys(obj)) { + if (RESERVED_RXDB_FIELDS.has(k)) continue + // shallow strip is enough for typical patches; deepen if you store nested meta + out[k] = obj[k] + } + return out as T +} diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index 874d7b2d7..0cbcf6d41 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -2,6 +2,7 @@ import { RxCollection, RxDocument, RxQuery, + clone, getFromMapOrCreate, lastOfArray } from "rxdb/plugins/core" @@ -19,12 +20,13 @@ import type { UtilsRecord, } from "@tanstack/db" import type { StandardSchemaV1 } from "@standard-schema/spec" +import { stripRxdbFields } from './helper' const debug = DebugModule.debug(`ts/db:electric`) export type RxDBCollectionConfig< - TExplicit extends object = object, + TExplicit extends object = Record, TSchema extends StandardSchemaV1 = never > = Omit< CollectionConfig< @@ -47,7 +49,7 @@ export type RxDBCollectionConfig< * @returns Collection options with utilities */ export function rxdbCollectionOptions< - TExplicit extends object = object, + TExplicit extends object = Record, TSchema extends StandardSchemaV1 = never >( config: RxDBCollectionConfig @@ -135,7 +137,7 @@ export function rxdbCollectionOptions< docs.forEach(d => { write({ type: 'insert', - value: d.toMutableJSON() + value: stripRxdbFields(clone(d.toJSON())) }) }) @@ -161,7 +163,7 @@ export function rxdbCollectionOptions< async function startOngoingFetch() { // Subscribe early and buffer live changes during initial load and ongoing sub = rxCollection.$.subscribe((ev) => { - const cur = ev.documentData as Row + const cur = stripRxdbFields(clone(ev.documentData as Row)) switch (ev.operation) { case 'INSERT': if (cur) queue({ type: 'insert', value: cur }) @@ -205,22 +207,41 @@ export function rxdbCollectionOptions< getKey, sync, onInsert: async (params) => { + console.log('ON INSERT CALLED') debug("insert", params) - return rxCollection.insert(params.value) + const newItems = params.transaction.mutations.map(m => m.modified) + return rxCollection.bulkUpsert(newItems as any).then(result => { + if (result.error.length > 0) { + throw result.error + } + return result.success + }) }, onUpdate: async (params) => { - debug("update", params.id, params.value) - const doc = await rxCollection.findOne(params.id).exec() - if (doc) { - await doc.patch(params.value) + debug("update", params) + const mutations = params.transaction.mutations.filter(m => m.type === 'update') + + for (const mutation of mutations) { + const newValue = stripRxdbFields(mutation.modified) + const id = (newValue as any)[primaryPath] + const doc = await rxCollection.findOne(id).exec() + if (!doc) { + continue + } + await doc.incrementalPatch(newValue as any) + console.log('UPDATE DONE') } }, onDelete: async (params) => { - debug("delete", params.id) - const doc = await rxCollection.findOne(params.id).exec() - if (doc) { - await doc.remove() - } + debug("delete", params) + const mutations = params.transaction.mutations.filter(m => m.type === 'delete') + const ids = mutations.map(mutation => (mutation.original as any)[primaryPath]) + return rxCollection.bulkRemove(ids).then(result => { + if (result.error.length > 0) { + throw result.error + } + return result.success + }) } } return collectionConfig; diff --git a/packages/rxdb-db-collection/tests/rxdb.test.ts b/packages/rxdb-db-collection/tests/rxdb.test.ts index b65946427..ce416568c 100644 --- a/packages/rxdb-db-collection/tests/rxdb.test.ts +++ b/packages/rxdb-db-collection/tests/rxdb.test.ts @@ -138,10 +138,48 @@ describe(`RxDB Integration`, () => { // deletes await doc.getLatest().remove() - console.log(':::::::::::::::::::::::::::::::::1') - console.dir(collection.get(`3`)) expect(collection.get(`3`)).toEqual(undefined) + await db.remove() + }) + + it(`should update RxDB when the collection changes data`, async () => { + const initialItems: Array = [ + { id: `1`, name: `Item 1` }, + { id: `2`, name: `Item 2` }, + ] + + const { collection, rxCollection, db } = await createTestState(initialItems); + + + // inserts + console.log(':::::::::::::::::::::::::::::::::::') + const xxx = collection.insert({ id: `3`, name: `inserted` }) + let doc = await rxCollection.findOne('3').exec(true) + expect(doc.name).toEqual('inserted') + + // updates + collection.update( + '3', + d => { + console.log('inside of update:') + console.dir(d) + d.name = 'updated' + }) + expect(collection.get(`3`).name).toEqual('updated') + await collection.stateWhenReady() + console.log('UPDATE OUTER DONE') + await rxCollection.database.requestIdlePromise() + doc = await rxCollection.findOne('3').exec(true) + expect(doc.name).toEqual('updated') + + + // deletes + collection.delete('3') + await rxCollection.database.requestIdlePromise() + console.log('DELETE OUTER DONE') + const mustNotBeFound = await rxCollection.findOne('3').exec() + expect(mustNotBeFound).toEqual(null) await db.remove() }) From 8fae7c9c97c9583a7517db16e7688f3c1183e47e Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Thu, 28 Aug 2025 16:16:32 +0200 Subject: [PATCH 04/18] ADD docs --- README.md | 3 +- docs/collections/rxdb-collection.md | 37 +++ docs/guides/collection-options-creator.md | 11 +- docs/installation.md | 11 + docs/overview.md | 51 +++- packages/rxdb-db-collection/src/rxdb.ts | 96 +++++--- .../rxdb-db-collection/tests/rxdb.test.ts | 220 ++++++++++++------ 7 files changed, 325 insertions(+), 104 deletions(-) create mode 100644 docs/collections/rxdb-collection.md diff --git a/README.md b/README.md index 6151c228c..de8cdd640 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ TanStack DB provides several collection types to support different backend integ - **`@tanstack/query-db-collection`** - Collections backed by [TanStack Query](https://tanstack.com/query) for REST APIs and GraphQL endpoints - **`@tanstack/electric-db-collection`** - Real-time sync collections powered by [ElectricSQL](https://electric-sql.com) for live database synchronization - **`@tanstack/trailbase-db-collection`** - Collections for [TrailBase](https://trailbase.io) backend integration +- **`@tanstack/rxdb-db-collection`** - Collections backed by [RxDB](https://rxdb.info), a client-side database with replication support and local persistence made for local-first apps. ## Framework integrations @@ -157,7 +158,7 @@ TanStack DB integrates with React & Vue with more on the way! ```bash npm install @tanstack/react-db # Optional: for specific collection types -npm install @tanstack/electric-db-collection @tanstack/query-db-collection +npm install @tanstack/electric-db-collection @tanstack/query-db-collection @tanstack/trailbase-db-collection @tanstack/rxdb-db-collection ``` Other framework integrations are in progress. diff --git a/docs/collections/rxdb-collection.md b/docs/collections/rxdb-collection.md new file mode 100644 index 000000000..f4cef0bfa --- /dev/null +++ b/docs/collections/rxdb-collection.md @@ -0,0 +1,37 @@ +--- +title: RxDB Collection +--- + +# RxDB Collection + +RxDB collections provide seamless integration between TanStack DB and [RxDB](https://rxdb.info), enabling automatic synchronization between your in-memory TanStack DB collections and RxDB's local-first, offline-ready database. + +## Overview + +The `@tanstack/rxdb-db-collection` package allows you to create collections that: +- Automatically mirror the state of an underlying RxDB collection +- Reactively update when RxDB documents change +- Support optimistic mutations with rollback on error +- Provide persistence handlers to keep RxDB in sync with TanStack DB transactions +- Work with RxDB's [replication features](https://rxdb.info/replication.html) for offline-first and sync scenarios +- Use on of RxDB's [storage engines](https://rxdb.info/rx-storage.html). + +## Installation + +```bash +npm install @tanstack/rxdb-db-collection rxdb @tanstack/db +``` + +```ts +import { createCollection } from '@tanstack/db' +import { rxdbCollectionOptions } from '@tanstack/rxdb-db-collection' + +// Assume you already have an RxDB collection instance: +const rxCollection = myDatabase.todos + +const todosCollection = createCollection( + rxdbCollectionOptions({ + rxCollection + }) +) +``` diff --git a/docs/guides/collection-options-creator.md b/docs/guides/collection-options-creator.md index 6dfe2bace..99b7adcdc 100644 --- a/docs/guides/collection-options-creator.md +++ b/docs/guides/collection-options-creator.md @@ -17,7 +17,7 @@ Collection options creators follow a consistent pattern: ## When to Create a Custom Collection You should create a custom collection when: -- You have a dedicated sync engine (like ElectricSQL, Trailbase, Firebase, or a custom WebSocket solution) +- You have a dedicated sync engine (like ElectricSQL, Trailbase, Firebase, RxDB or a custom WebSocket solution) - You need specific sync behaviors that aren't covered by the query collection - You want to integrate with a backend that has its own sync protocol @@ -331,6 +331,7 @@ For complete, production-ready examples, see the collection packages in the TanS - **[@tanstack/query-collection](https://github.com/TanStack/db/tree/main/packages/query-collection)** - Pattern A: User-provided handlers with full refetch strategy - **[@tanstack/trailbase-collection](https://github.com/TanStack/db/tree/main/packages/trailbase-collection)** - Pattern B: Built-in handlers with ID-based tracking - **[@tanstack/electric-collection](https://github.com/TanStack/db/tree/main/packages/electric-collection)** - Pattern A: Transaction ID tracking with complex sync protocols +- **[@tanstack/rxdb-collection](https://github.com/TanStack/db/tree/main/packages/rxdb-collection)** - Pattern B: Built-in handlers that bridge [RxDB](https://rxdb.info) change streams into TanStack DB's sync lifecycle ### Key Lessons from Production Collections @@ -349,6 +350,14 @@ For complete, production-ready examples, see the collection packages in the TanS - Demonstrates advanced deduplication techniques - Shows how to wrap user handlers with sync coordination +**From RxDB Collection:** +- Uses RxDB's built-in queries and change streams +- Uses `RxCollection.$` to subscribe to inserts/updates/deletes and forward them to TanStack DB with begin - write - commit +- Implements built-in mutation handlers (onInsert, onUpdate, onDelete) that call RxDB APIs (bulkUpsert, incrementalPatch, bulkRemove) + +Pattern: Built-in handlers, great for offline-first and P2P use cases + + ## Complete Example: WebSocket Collection Here's a complete example of a WebSocket-based collection options creator that demonstrates the full round-trip flow: diff --git a/docs/installation.md b/docs/installation.md index 8948c0282..dfe4c59f6 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -85,3 +85,14 @@ npm install @tanstack/trailbase-db-collection ``` Use `trailBaseCollectionOptions` to sync records from TrailBase's Record APIs with built-in subscription support. + +### RxDB Collection + +For offline-first apps and local persistence with [RxDB](https://rxdb.info): + +```sh +npm install @tanstack/rxdb-db-collection +``` + +Use `rxdbCollectionOptions` to bridge an [RxDB collection](https://rxdb.info/rx-collection.html) into TanStack DB. +This gives you reactive TanStack DB collections backed by RxDB's powerful local-first database, replication, and conflict handling features. diff --git a/docs/overview.md b/docs/overview.md index 92ed98198..acdb4c417 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -154,8 +154,9 @@ There are a number of built-in collection types: 1. [`QueryCollection`](#querycollection) to load data into collections using [TanStack Query](https://tanstack.com/query) 2. [`ElectricCollection`](#electriccollection) to sync data into collections using [ElectricSQL](https://electric-sql.com) 3. [`TrailBaseCollection`](#trailbasecollection) to sync data into collections using [TrailBase](https://trailbase.io) -4. [`LocalStorageCollection`](#localstoragecollection) for small amounts of local-only state that syncs across browser tabs -5. [`LocalOnlyCollection`](#localonlycollection) for in-memory client data or UI state +4. [`RxDBCollection`](#rxdbcollection) to integrate with [RxDB](https://rxdb.info) for local persistence and sync +5. [`LocalStorageCollection`](#localstoragecollection) for small amounts of local-only state that syncs across browser tabs +6. [`LocalOnlyCollection`](#localonlycollection) for in-memory client data or UI state You can also use: @@ -298,6 +299,52 @@ This collection requires the following TrailBase-specific options: A new collections doesn't start syncing until you call `collection.preload()` or you query it. +#### `RxDBCollection` + +[RxDB](https://rxdb.info) is a client-side database for JavaScript apps with replication, conflict resolution, and offline-first features. +Use `rxdbCollectionOptions` from `@tanstack/rxdb-db-collection` to integrate an RxDB collection with TanStack DB: + +```ts +import { createCollection } from "@tanstack/react-db" +import { rxdbCollectionOptions } from "@tanstack/rxdb-db-collection" +import { createRxDatabase } from "rxdb" + +const db = await createRxDatabase({ + name: "mydb", + storage: getRxStorageMemory(), +}) +await db.addCollections({ + todos: { + schema: { + version: 0, + primaryKey: "id", + type: "object", + properties: { + id: { type: "string", maxLength: 100 }, + text: { type: "string" }, + completed: { type: "boolean" }, + }, + }, + }, +}) + +// Wrap the RxDB collection with TanStack DB +export const todoCollection = createCollection( + rxdbCollectionOptions({ + rxCollection: db.todos, + startSync: true + }) +) +``` + +With this integration: + +- TanStack DB subscribes to RxDB's change streams and reflects updates, deletes, and inserts in real-time. +- You get local-first sync when RxDB replication is configured. +- Mutation handlers (onInsert, onUpdate, onDelete) are implemented using RxDB's APIs (bulkUpsert, incrementalPatch, bulkRemove). + +This makes RxDB a great choice for apps that need local-first storage, replication, or peer-to-peer sync combined with TanStack DB's live queries and transaction lifecycle. + #### `LocalStorageCollection` localStorage collections store small amounts of local-only state that persists across browser sessions and syncs across browser tabs in real-time. All data is stored under a single localStorage key and automatically synchronized using storage events. diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index 0cbcf6d41..fdf3b73e9 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -1,10 +1,15 @@ import { + FilledMangoQuery, RxCollection, RxDocument, + RxDocumentData, RxQuery, clone, + ensureNotFalsy, getFromMapOrCreate, - lastOfArray + lastOfArray, + prepareQuery, + rxStorageWriteErrorToRxError } from "rxdb/plugins/core" import type { Subscription } from 'rxjs' @@ -25,19 +30,25 @@ import { stripRxdbFields } from './helper' const debug = DebugModule.debug(`ts/db:electric`) +/** + * Used in tests to ensure proper cleanup + */ +export const OPEN_RXDB_SUBSCRIPTIONS = new WeakMap>() + + export type RxDBCollectionConfig< TExplicit extends object = Record, TSchema extends StandardSchemaV1 = never > = Omit< CollectionConfig< - TExplicit, - string, // because RxDB primary keys must be strings + ResolveType, // ← use Row here + string, // key is string TSchema >, - "insert" | "update" | "delete" | "getKey" | "sync" + 'insert' | 'update' | 'delete' | 'getKey' | 'sync' > & { rxCollection: RxCollection -}; +} /** * Creates RxDB collection options for use with a standard Collection @@ -65,7 +76,7 @@ export function rxdbCollectionOptions< // "getKey" const primaryPath = rxCollection.schema.primaryPath const getKey: CollectionConfig['getKey'] = (item) => { - const key: string = (item as any)[primaryPath] + const key: string = (item as any)[primaryPath] as string return key } @@ -78,56 +89,66 @@ export function rxdbCollectionOptions< type SyncParams = Parameters['sync']>[0] const sync: SyncConfig = { sync: (params: SyncParams) => { - console.log('SYCN START!!!') const { begin, write, commit, markReady } = params let ready = false async function initialFetch() { - console.log('initialFetch() START'); /** * RxDB stores a last-write-time * which can be used to "sort" document writes, * so for initial sync we iterate over that. */ - let cursor: RxDocument | undefined = undefined + let cursor: RxDocumentData | undefined = undefined const syncBatchSize = 1000 // make this configureable begin() while (!ready) { - console.log('initialFetch() loooooop'); - let query: RxQuery[], unknown, unknown> + let query: FilledMangoQuery if (cursor) { - query = rxCollection.find({ + query = { selector: { $or: [ - { '_meta.lwt': { $gt: cursor._data._meta.lwt } }, + { '_meta.lwt': { $gt: (cursor._meta.lwt as number) } }, { - '_meta.lwt': cursor._data._meta.lwt, + '_meta.lwt': cursor._meta.lwt, [primaryPath]: { - $gt: cursor.primary + $gt: cursor[primaryPath] }, } ] - }, + } as any, sort: [ { '_meta.lwt': 'asc' }, - { [primaryPath]: 'asc' } + { [primaryPath]: 'asc' } as any ], - limit: syncBatchSize - }) + limit: syncBatchSize, + skip: 0 + } } else { - query = rxCollection.find({ + query = { selector: {}, sort: [ { '_meta.lwt': 'asc' }, - { [primaryPath]: 'asc' } + { [primaryPath]: 'asc' } as any ], - limit: syncBatchSize - }) + limit: syncBatchSize, + skip: 0 + } } - const docs = await query.exec(); - console.dir(docs.map(d => d.toJSON())) + /** + * Instead of doing a RxCollection.query(), + * we directly query the storage engine of the RxCollection so we do not use the + * RxCollection document cache because it likely wont be used anyway + * since most queries will run directly on the tanstack-db side. + */ + const preparedQuery = prepareQuery( + rxCollection.storageInstance.schema, + query + ); + const result = await rxCollection.storageInstance.query(preparedQuery) + const docs = result.documents + cursor = lastOfArray(docs) if (docs.length === 0) { ready = true @@ -137,13 +158,11 @@ export function rxdbCollectionOptions< docs.forEach(d => { write({ type: 'insert', - value: stripRxdbFields(clone(d.toJSON())) + value: stripRxdbFields(clone(d)) as any }) }) } - console.log('initialFetch() DONE'); - commit() } @@ -176,6 +195,13 @@ export function rxdbCollectionOptions< break } }) + + const subs = getFromMapOrCreate( + OPEN_RXDB_SUBSCRIPTIONS, + rxCollection, + () => new Set() + ) + subs.add(sub) } @@ -196,7 +222,15 @@ export function rxdbCollectionOptions< start() - return () => sub.unsubscribe() + return () => { + const subs = getFromMapOrCreate( + OPEN_RXDB_SUBSCRIPTIONS, + rxCollection, + () => new Set() + ) + subs.delete(sub) + sub.unsubscribe() + } }, // Expose the getSyncMetadata function getSyncMetadata: undefined, @@ -207,12 +241,11 @@ export function rxdbCollectionOptions< getKey, sync, onInsert: async (params) => { - console.log('ON INSERT CALLED') debug("insert", params) const newItems = params.transaction.mutations.map(m => m.modified) return rxCollection.bulkUpsert(newItems as any).then(result => { if (result.error.length > 0) { - throw result.error + throw rxStorageWriteErrorToRxError(ensureNotFalsy(result.error[0])) } return result.success }) @@ -229,7 +262,6 @@ export function rxdbCollectionOptions< continue } await doc.incrementalPatch(newValue as any) - console.log('UPDATE DONE') } }, onDelete: async (params) => { diff --git a/packages/rxdb-db-collection/tests/rxdb.test.ts b/packages/rxdb-db-collection/tests/rxdb.test.ts index ce416568c..3637a7066 100644 --- a/packages/rxdb-db-collection/tests/rxdb.test.ts +++ b/packages/rxdb-db-collection/tests/rxdb.test.ts @@ -4,7 +4,7 @@ import { createCollection, createTransaction, } from "@tanstack/db" -import { RxDBCollectionConfig, rxdbCollectionOptions } from "../src/rxdb" +import { OPEN_RXDB_SUBSCRIPTIONS, RxDBCollectionConfig, rxdbCollectionOptions } from "../src/rxdb" import type { Collection, InsertMutationFnParams, @@ -15,7 +15,7 @@ import type { UtilsRecord, } from "@tanstack/db" import type { StandardSchemaV1 } from "@standard-schema/spec" -import { RxCollection, addRxPlugin, createRxDatabase } from 'rxdb/plugins/core' +import { RxCollection, addRxPlugin, createRxDatabase, getFromMapOrCreate } from 'rxdb/plugins/core' import { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode' import { getRxStorageMemory } from 'rxdb/plugins/storage-memory' import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv' @@ -68,7 +68,8 @@ describe(`RxDB Integration`, () => { maxLength: 100 }, name: { - type: 'string' + type: 'string', + maxLength: 9 } } } @@ -97,90 +98,173 @@ describe(`RxDB Integration`, () => { } - it(`should initialize and fetch initial data`, async () => { - const initialItems: Array = [ - { id: `1`, name: `Item 1` }, - { id: `2`, name: `Item 2` }, - ] + describe('sync', () => { - const { collection, db } = await createTestState(initialItems); - // Verify the collection state contains our items - expect(collection.size).toBe(initialItems.length) - expect(collection.get(`1`)).toEqual(initialItems[0]) - expect(collection.get(`2`)).toEqual(initialItems[1]) + it(`should initialize and fetch initial data`, async () => { + const initialItems: Array = [ + { id: `1`, name: `Item 1` }, + { id: `2`, name: `Item 2` }, + ] - // Verify the synced data - expect(collection.syncedData.size).toBe(initialItems.length) - expect(collection.syncedData.get(`1`)).toEqual(initialItems[0]) - expect(collection.syncedData.get(`2`)).toEqual(initialItems[1]) + const { collection, db } = await createTestState(initialItems); - await db.remove() - }) + // Verify the collection state contains our items + expect(collection.size).toBe(initialItems.length) + expect(collection.get(`1`)).toEqual(initialItems[0]) + expect(collection.get(`2`)).toEqual(initialItems[1]) - it(`should update the collection when RxDB changes data`, async () => { - const initialItems: Array = [ - { id: `1`, name: `Item 1` }, - { id: `2`, name: `Item 2` }, - ] + // Verify the synced data + expect(collection.syncedData.size).toBe(initialItems.length) + expect(collection.syncedData.get(`1`)).toEqual(initialItems[0]) + expect(collection.syncedData.get(`2`)).toEqual(initialItems[1]) - const { collection, rxCollection, db } = await createTestState(initialItems); + await db.remove() + }) + it(`should update the collection when RxDB changes data`, async () => { + const initialItems: Array = [ + { id: `1`, name: `Item 1` }, + { id: `2`, name: `Item 2` }, + ] - // inserts - const doc = await rxCollection.insert({ id: '3', name: 'inserted' }) - expect(collection.get(`3`).name).toEqual('inserted') + const { collection, rxCollection, db } = await createTestState(initialItems); - // updates - await doc.getLatest().patch({ name: 'updated' }) - expect(collection.get(`3`).name).toEqual('updated') + // inserts + const doc = await rxCollection.insert({ id: '3', name: 'inserted' }) + expect(collection.get(`3`).name).toEqual('inserted') - // deletes - await doc.getLatest().remove() - expect(collection.get(`3`)).toEqual(undefined) + // updates + await doc.getLatest().patch({ name: 'updated' }) + expect(collection.get(`3`).name).toEqual('updated') - await db.remove() - }) - it(`should update RxDB when the collection changes data`, async () => { - const initialItems: Array = [ - { id: `1`, name: `Item 1` }, - { id: `2`, name: `Item 2` }, - ] + // deletes + await doc.getLatest().remove() + expect(collection.get(`3`)).toEqual(undefined) - const { collection, rxCollection, db } = await createTestState(initialItems); + await db.remove() + }) + it(`should update RxDB when the collection changes data`, async () => { + const initialItems: Array = [ + { id: `1`, name: `Item 1` }, + { id: `2`, name: `Item 2` }, + ] - // inserts - console.log(':::::::::::::::::::::::::::::::::::') - const xxx = collection.insert({ id: `3`, name: `inserted` }) - let doc = await rxCollection.findOne('3').exec(true) - expect(doc.name).toEqual('inserted') + const { collection, rxCollection, db } = await createTestState(initialItems); - // updates - collection.update( - '3', - d => { - console.log('inside of update:') - console.dir(d) - d.name = 'updated' - }) - expect(collection.get(`3`).name).toEqual('updated') - await collection.stateWhenReady() - console.log('UPDATE OUTER DONE') - await rxCollection.database.requestIdlePromise() - doc = await rxCollection.findOne('3').exec(true) - expect(doc.name).toEqual('updated') + // inserts + const tx = collection.insert({ id: `3`, name: `inserted` }) + await tx.isPersisted.promise + let doc = await rxCollection.findOne('3').exec(true) + expect(doc.name).toEqual('inserted') + + // updates + collection.update( + '3', + d => { + d.name = 'updated' + } + ) + expect(collection.get(`3`).name).toEqual('updated') + await collection.stateWhenReady() + await rxCollection.database.requestIdlePromise() + doc = await rxCollection.findOne('3').exec(true) + expect(doc.name).toEqual('updated') + + + // deletes + collection.delete('3') + await rxCollection.database.requestIdlePromise() + const mustNotBeFound = await rxCollection.findOne('3').exec() + expect(mustNotBeFound).toEqual(null) + + await db.remove() + }) + }); + + describe(`lifecycle management`, () => { + it(`should call unsubscribe when collection is cleaned up`, async () => { + const { collection, rxCollection, db } = await createTestState(); - // deletes - collection.delete('3') - await rxCollection.database.requestIdlePromise() - console.log('DELETE OUTER DONE') - const mustNotBeFound = await rxCollection.findOne('3').exec() - expect(mustNotBeFound).toEqual(null) + await collection.cleanup() + + const subs = getFromMapOrCreate( + OPEN_RXDB_SUBSCRIPTIONS, + rxCollection, + () => new Set() + ) + expect(subs.size).toEqual(0) + + + await db.remove() + }) - await db.remove() + it(`should restart sync when collection is accessed after cleanup`, async () => { + const initialItems: Array = [ + { id: `1`, name: `Item 1` }, + { id: `2`, name: `Item 2` }, + ] + const { collection, rxCollection, db } = await createTestState(initialItems); + + await collection.cleanup() + await flushPromises() + expect(collection.status).toBe(`cleaned-up`) + + // insert into RxDB while cleaned-up + await rxCollection.insert({ id: '3', name: 'Item 3' }) + + // Access collection data to restart sync + const unsubscribe = collection.subscribeChanges(() => { }) + + await collection.toArrayWhenReady() + expect(collection.get(`3`).name).toEqual('Item 3') + + + unsubscribe() + await db.remove() + }) }) + + describe('error handling', () => { + it('should rollback the transaction on invalid data that does not match the RxCollection schema', async () => { + const initialItems: Array = [ + { id: `1`, name: `Item 1` }, + { id: `2`, name: `Item 2` }, + ] + const { collection, db } = await createTestState(initialItems); + + // INSERT + await expect(async () => { + const tx = await collection.insert({ + id: '3', + name: 'invalid', + foo: 'bar' + }) + await tx.isPersisted.promise + }).rejects.toThrow(/schema validation error/) + expect(collection.has('3')).toBe(false) + + // UPDATE + await expect(async () => { + const tx = await collection.update( + '2', + d => { + d.name = 'invalid' + d.foo = 'bar' + } + ) + await tx.isPersisted.promise + }).rejects.toThrow(/schema validation error/) + expect(collection.get('2').name).toBe('Item 2') + + + await db.remove() + }) + }) + + }); From 75cca39dd479cd8c65751585abb6d37772d3a143 Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:47:12 +0200 Subject: [PATCH 05/18] ADD docs --- docs/collections/rxdb-collection.md | 105 ++++++++++++++++++++++-- docs/config.json | 12 +++ packages/rxdb-db-collection/src/rxdb.ts | 27 +++--- 3 files changed, 126 insertions(+), 18 deletions(-) diff --git a/docs/collections/rxdb-collection.md b/docs/collections/rxdb-collection.md index f4cef0bfa..38b508479 100644 --- a/docs/collections/rxdb-collection.md +++ b/docs/collections/rxdb-collection.md @@ -4,7 +4,8 @@ title: RxDB Collection # RxDB Collection -RxDB collections provide seamless integration between TanStack DB and [RxDB](https://rxdb.info), enabling automatic synchronization between your in-memory TanStack DB collections and RxDB's local-first, offline-ready database. +RxDB collections provide seamless integration between TanStack DB and [RxDB](https://rxdb.info), enabling automatic synchronization between your in-memory TanStack DB collections and RxDB's local-first database. Giving you offline-ready persistence, and powerful sync capabilities with a wide range of backends. + ## Overview @@ -13,25 +14,115 @@ The `@tanstack/rxdb-db-collection` package allows you to create collections that - Reactively update when RxDB documents change - Support optimistic mutations with rollback on error - Provide persistence handlers to keep RxDB in sync with TanStack DB transactions +- Sync across browser tabs - changes in one tab are reflected in RxDB and TanStack DB collections in all tabs +- Use one of RxDB's [storage engines](https://rxdb.info/rx-storage.html). - Work with RxDB's [replication features](https://rxdb.info/replication.html) for offline-first and sync scenarios -- Use on of RxDB's [storage engines](https://rxdb.info/rx-storage.html). +- Leverage RxDB's [replication plugins](https://rxdb.info/replication.html) to sync with CouchDB, MongoDB, Supabase, REST APIs, GraphQL, WebRTC (P2P) and more. + -## Installation +## 1. Installation ```bash npm install @tanstack/rxdb-db-collection rxdb @tanstack/db ``` + +### 2. Create an RxDatabase and RxCollection + +```ts +import { createRxDatabase, addRxPlugin } from 'rxdb/plugins/core' +import { getRxStorageDexie } from 'rxdb/plugins/storage-dexie' // Browser (IndexedDB via Dexie.js) + +// add json-schema validation (optional) +import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv'; + +// Enable dev mode (optional, recommended during development) +import { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode' +addRxPlugin(RxDBDevModePlugin) + +type Todo = { id: string; text: string; completed: boolean } + +const db = await createRxDatabase({ + name: 'my-todos', + storage: wrappedValidateAjvStorage({ + storage: getRxStorageDexie() + }) +}) + +await db.addCollections({ + todos: { + schema: { + title: 'todos', + version: 0, + type: 'object', + primaryKey: 'id', + properties: { + id: { type: 'string', maxLength: 100 }, + text: { type: 'string' }, + completed: { type: 'boolean' }, + }, + required: ['id', 'text', 'completed'], + }, + }, +}) +``` + + +### 3. (optional) sync with a backend +```ts +import { replicateRxCollection } from 'rxdb/plugins/replication' +const replicationState = replicateRxCollection({ + collection: db.todos, + pull: { handler: myPullHandler }, + push: { handler: myPushHandler }, +}) +``` + +### 4. Wrap the RxDB collection with TanStack DB + ```ts import { createCollection } from '@tanstack/db' import { rxdbCollectionOptions } from '@tanstack/rxdb-db-collection' -// Assume you already have an RxDB collection instance: -const rxCollection = myDatabase.todos - const todosCollection = createCollection( rxdbCollectionOptions({ - rxCollection + rxCollection: myDatabase.todos, + startSync: true, // start ingesting RxDB data immediately }) ) ``` + + +Now `todosCollection` is a reactive TanStack DB collection driven by RxDB: + +- Writes via `todosCollection.insert/update/delete` persist to RxDB. +- Direct writes in RxDB (or via replication) flow into the TanStack collection via change streams. + + + +## Configuration Options + +The `rxdbCollectionOptions` function accepts the following options: + +### Required + +- `rxCollection`: The underlying [RxDB collection](https://rxdb.info/rx-collection.html) + +### Optional + +- `id`: Unique identifier for the collection +- `schema`: Schema for validating items. RxDB already has schema validation but having additional validation on the TanStack DB side can help to unify error handling between different tanstack collections. +- `startSync`: Whether to start syncing immediately (default: true) +- `onInsert, onUpdate, onDelete`: Override default persistence handlers + + +By default, TanStack DB writes are persisted to RxDB using bulkUpsert, patch, and bulkRemove. + + +## Syncing with Backends + +Replication and sync in RxDB run independently of TanStack DB. You set up replication directly on your RxCollection using RxDB's replication plugins (for CouchDB, GraphQL, WebRTC, REST APIs, etc.). + +When replication runs, it pulls and pushes changes to the backend and applies them to the RxDB collection. Since the TanStack DB integration subscribes to the RxDB change stream, any changes applied by replication are automatically reflected in your TanStack DB collection. + +This separation of concerns means you configure replication entirely in RxDB, and TanStack DB automatically benefits: your TanStack collections always stay up to date with whatever sync strategy you choose. diff --git a/docs/config.json b/docs/config.json index 90a7225d3..155ef8492 100644 --- a/docs/config.json +++ b/docs/config.json @@ -88,6 +88,10 @@ { "label": "Electric Collection", "to": "collections/electric-collection" + }, + { + "label": "RxDB Collection", + "to": "collections/rxdb-collection" } ] }, @@ -137,6 +141,14 @@ { "label": "queryCollectionOptions", "to": "reference/query-db-collection/functions/querycollectionoptions" + }, + { + "label": "RxDB DB Collection", + "to": "reference/rxdb-db-collection/index" + }, + { + "label": "rxdbCollectionOptions", + "to": "reference/rxdb-db-collection/functions/rxdbcollectionoptions" } ], "frameworks": [ diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index fdf3b73e9..0411e8a03 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -1,9 +1,7 @@ import { FilledMangoQuery, RxCollection, - RxDocument, RxDocumentData, - RxQuery, clone, ensureNotFalsy, getFromMapOrCreate, @@ -13,21 +11,16 @@ import { } from "rxdb/plugins/core" import type { Subscription } from 'rxjs' -import { Store } from "@tanstack/store" import DebugModule from "debug" import type { CollectionConfig, - DeleteMutationFnParams, - InsertMutationFnParams, ResolveType, SyncConfig, - UpdateMutationFnParams, - UtilsRecord, } from "@tanstack/db" import type { StandardSchemaV1 } from "@standard-schema/spec" import { stripRxdbFields } from './helper' -const debug = DebugModule.debug(`ts/db:electric`) +const debug = DebugModule.debug(`ts/db:rxdb`) /** @@ -36,6 +29,18 @@ const debug = DebugModule.debug(`ts/db:electric`) export const OPEN_RXDB_SUBSCRIPTIONS = new WeakMap>() +/** + * Configuration interface for Electric collection options + * @template TExplicit - The explicit type of items in the collection (highest priority). Use the document type of your RxCollection here. + * @template TSchema - The schema type for validation and type inference (second priority) + * + * @remarks + * Type resolution follows a priority order: + * 1. If you provide an explicit type via generic parameter, it will be used + * 2. If no explicit type is provided but a schema is, the schema's output type will be inferred + * + * You should provide EITHER an explicit type OR a schema, but not both, as they would conflict. + */ export type RxDBCollectionConfig< TExplicit extends object = Record, TSchema extends StandardSchemaV1 = never @@ -47,6 +52,9 @@ export type RxDBCollectionConfig< >, 'insert' | 'update' | 'delete' | 'getKey' | 'sync' > & { + /** + * The RxCollection from a RxDB Database instance. + */ rxCollection: RxCollection } @@ -55,7 +63,6 @@ export type RxDBCollectionConfig< * * @template TExplicit - The explicit type of items in the collection (highest priority) * @template TSchema - The schema type for validation and type inference (second priority) - * @template TFallback - The fallback type if no explicit or schema type is provided * @param config - Configuration options for the Electric collection * @returns Collection options with utilities */ @@ -70,8 +77,6 @@ export function rxdbCollectionOptions< const { ...restConfig } = config const rxCollection = config.rxCollection - debug("wrapping RxDB collection", name) - // "getKey" const primaryPath = rxCollection.schema.primaryPath From 024e0651d709e0a766498e01c5251699eb2b3f0f Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:05:12 +0200 Subject: [PATCH 06/18] UPDATE docs and tests --- docs/collections/rxdb-collection.md | 5 +- docs/guides/collection-options-creator.md | 5 +- packages/rxdb-db-collection/src/helper.ts | 1 - packages/rxdb-db-collection/src/index.ts | 1 + packages/rxdb-db-collection/src/rxdb.ts | 24 +++++- .../rxdb-db-collection/tests/rxdb.test.ts | 86 ++++++++++++------- 6 files changed, 78 insertions(+), 44 deletions(-) diff --git a/docs/collections/rxdb-collection.md b/docs/collections/rxdb-collection.md index 38b508479..6ed4ae0de 100644 --- a/docs/collections/rxdb-collection.md +++ b/docs/collections/rxdb-collection.md @@ -113,10 +113,9 @@ The `rxdbCollectionOptions` function accepts the following options: - `id`: Unique identifier for the collection - `schema`: Schema for validating items. RxDB already has schema validation but having additional validation on the TanStack DB side can help to unify error handling between different tanstack collections. - `startSync`: Whether to start syncing immediately (default: true) -- `onInsert, onUpdate, onDelete`: Override default persistence handlers - +- `onInsert, onUpdate, onDelete`: Override default persistence handlers. By default, TanStack DB writes are persisted to RxDB using bulkUpsert, patch, and bulkRemove. +- `syncBatchSize`: The maximum number of documents fetched per batch during the initial sync from RxDB into TanStack DB (default: 1000). Larger values reduce round trips but use more memory; smaller values are lighter but may increase query calls. Note that this only affects the initial sync. Ongoing live updates are streamed one by one via RxDB's change feed. -By default, TanStack DB writes are persisted to RxDB using bulkUpsert, patch, and bulkRemove. ## Syncing with Backends diff --git a/docs/guides/collection-options-creator.md b/docs/guides/collection-options-creator.md index 99b7adcdc..058bb7b2c 100644 --- a/docs/guides/collection-options-creator.md +++ b/docs/guides/collection-options-creator.md @@ -352,12 +352,9 @@ For complete, production-ready examples, see the collection packages in the TanS **From RxDB Collection:** - Uses RxDB's built-in queries and change streams -- Uses `RxCollection.$` to subscribe to inserts/updates/deletes and forward them to TanStack DB with begin - write - commit +- Uses `RxCollection.$` to subscribe to inserts/updates/deletes and forward them to TanStack DB with begin-write-commit - Implements built-in mutation handlers (onInsert, onUpdate, onDelete) that call RxDB APIs (bulkUpsert, incrementalPatch, bulkRemove) -Pattern: Built-in handlers, great for offline-first and P2P use cases - - ## Complete Example: WebSocket Collection Here's a complete example of a WebSocket-based collection options creator that demonstrates the full round-trip flow: diff --git a/packages/rxdb-db-collection/src/helper.ts b/packages/rxdb-db-collection/src/helper.ts index 86b39f4cb..57678bb37 100644 --- a/packages/rxdb-db-collection/src/helper.ts +++ b/packages/rxdb-db-collection/src/helper.ts @@ -10,7 +10,6 @@ export function stripRxdbFields>(obj: T): T { const out: any = Array.isArray(obj) ? [] : {} for (const k of Object.keys(obj)) { if (RESERVED_RXDB_FIELDS.has(k)) continue - // shallow strip is enough for typical patches; deepen if you store nested meta out[k] = obj[k] } return out as T diff --git a/packages/rxdb-db-collection/src/index.ts b/packages/rxdb-db-collection/src/index.ts index 93145f7d1..4d3f5981c 100644 --- a/packages/rxdb-db-collection/src/index.ts +++ b/packages/rxdb-db-collection/src/index.ts @@ -1 +1,2 @@ export * from "./rxdb" +export * from "./helper" diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index 0411e8a03..6e1838f0b 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -40,14 +40,15 @@ export const OPEN_RXDB_SUBSCRIPTIONS = new WeakMap, TSchema extends StandardSchemaV1 = never > = Omit< CollectionConfig< - ResolveType, // ← use Row here - string, // key is string + ResolveType, + string, TSchema >, 'insert' | 'update' | 'delete' | 'getKey' | 'sync' @@ -56,6 +57,23 @@ export type RxDBCollectionConfig< * The RxCollection from a RxDB Database instance. */ rxCollection: RxCollection + + /** + * The maximum number of documents to read from the RxDB collection + * in a single batch during the initial sync between RxDB and the + * in-memory TanStack DB collection. + * + * @remarks + * - Defaults to `1000` if not specified. + * - Larger values reduce the number of round trips to the storage + * engine but increase memory usage per batch. + * - Smaller values may lower memory usage and allow earlier + * streaming of initial results, at the cost of more query calls. + * + * Adjust this depending on your expected collection size and + * performance characteristics of the chosen RxDB storage adapter. + */ + syncBatchSize?: number } /** @@ -104,7 +122,7 @@ export function rxdbCollectionOptions< * so for initial sync we iterate over that. */ let cursor: RxDocumentData | undefined = undefined - const syncBatchSize = 1000 // make this configureable + const syncBatchSize = config.syncBatchSize ? config.syncBatchSize : 1000 begin() while (!ready) { diff --git a/packages/rxdb-db-collection/tests/rxdb.test.ts b/packages/rxdb-db-collection/tests/rxdb.test.ts index 3637a7066..40ee231f3 100644 --- a/packages/rxdb-db-collection/tests/rxdb.test.ts +++ b/packages/rxdb-db-collection/tests/rxdb.test.ts @@ -1,21 +1,23 @@ -import { beforeEach, describe, expect, it, vi } from "vitest" +import { describe, expect, it, vi } from "vitest" import { - CollectionImpl, createCollection, - createTransaction, } from "@tanstack/db" -import { OPEN_RXDB_SUBSCRIPTIONS, RxDBCollectionConfig, rxdbCollectionOptions } from "../src/rxdb" +import { + OPEN_RXDB_SUBSCRIPTIONS, + RxDBCollectionConfig, + rxdbCollectionOptions +} from "../src/rxdb" import type { Collection, - InsertMutationFnParams, - MutationFnParams, - PendingMutation, - Transaction, - TransactionWithMutations, UtilsRecord, } from "@tanstack/db" import type { StandardSchemaV1 } from "@standard-schema/spec" -import { RxCollection, addRxPlugin, createRxDatabase, getFromMapOrCreate } from 'rxdb/plugins/core' +import { + RxCollection, + addRxPlugin, + createRxDatabase, + getFromMapOrCreate +} from 'rxdb/plugins/core' import { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode' import { getRxStorageMemory } from 'rxdb/plugins/storage-memory' import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv' @@ -46,6 +48,14 @@ describe(`RxDB Integration`, () => { >; let dbNameId = 0; + function getTestData(amount: number): Array { + return new Array(amount).fill(0).map((_v, i) => { + return { + id: (i + 1) + '', + name: 'Item ' + (i + 1) + } + }) + } async function createTestState( initialDocs: TestDocType[] = [], config: Partial> = {} @@ -84,6 +94,11 @@ describe(`RxDB Integration`, () => { const options = rxdbCollectionOptions({ rxCollection: rxCollection, startSync: true, + /** + * In tests we use a small batch size + * to ensure iteration works. + */ + syncBatchSize: 10, ...config }) @@ -97,15 +112,9 @@ describe(`RxDB Integration`, () => { } } - describe('sync', () => { - - it(`should initialize and fetch initial data`, async () => { - const initialItems: Array = [ - { id: `1`, name: `Item 1` }, - { id: `2`, name: `Item 2` }, - ] + const initialItems = getTestData(2) const { collection, db } = await createTestState(initialItems); @@ -122,11 +131,31 @@ describe(`RxDB Integration`, () => { await db.remove() }) + it('should initialize and fetch initial data with many documents', async () => { + const docsAmount = 25; // > 10 to force multiple batches + const initialItems = getTestData(docsAmount); + const { collection, db } = await createTestState(initialItems); + + // All docs should be present after initial sync + expect(collection.size).toBe(docsAmount); + expect(collection.syncedData.size).toBe(docsAmount); + + // Spot-check a few positions + expect(collection.get('1')).toEqual({ id: '1', name: 'Item 1' }); + expect(collection.get('10')).toEqual({ id: '10', name: 'Item 10' }); + expect(collection.get('11')).toEqual({ id: '11', name: 'Item 11' }); + expect(collection.get('25')).toEqual({ id: '25', name: 'Item 25' }); + + // Ensure no gaps + for (let i = 1; i <= docsAmount; i++) { + expect(collection.has(String(i))).toBe(true); + } + + await db.remove() + }) + it(`should update the collection when RxDB changes data`, async () => { - const initialItems: Array = [ - { id: `1`, name: `Item 1` }, - { id: `2`, name: `Item 2` }, - ] + const initialItems = getTestData(2) const { collection, rxCollection, db } = await createTestState(initialItems); @@ -148,10 +177,7 @@ describe(`RxDB Integration`, () => { }) it(`should update RxDB when the collection changes data`, async () => { - const initialItems: Array = [ - { id: `1`, name: `Item 1` }, - { id: `2`, name: `Item 2` }, - ] + const initialItems = getTestData(2) const { collection, rxCollection, db } = await createTestState(initialItems); @@ -204,10 +230,7 @@ describe(`RxDB Integration`, () => { }) it(`should restart sync when collection is accessed after cleanup`, async () => { - const initialItems: Array = [ - { id: `1`, name: `Item 1` }, - { id: `2`, name: `Item 2` }, - ] + const initialItems = getTestData(2) const { collection, rxCollection, db } = await createTestState(initialItems); await collection.cleanup() @@ -231,10 +254,7 @@ describe(`RxDB Integration`, () => { describe('error handling', () => { it('should rollback the transaction on invalid data that does not match the RxCollection schema', async () => { - const initialItems: Array = [ - { id: `1`, name: `Item 1` }, - { id: `2`, name: `Item 2` }, - ] + const initialItems = getTestData(2) const { collection, db } = await createTestState(initialItems); // INSERT From 25b7a5f6537cd71a8a015c79c1df93ea02f838a7 Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:06:29 +0200 Subject: [PATCH 07/18] FIX typos --- packages/rxdb-db-collection/CHANGELOG.md | 4 +++- packages/rxdb-db-collection/src/rxdb.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/rxdb-db-collection/CHANGELOG.md b/packages/rxdb-db-collection/CHANGELOG.md index 62f848a93..67220f792 100644 --- a/packages/rxdb-db-collection/CHANGELOG.md +++ b/packages/rxdb-db-collection/CHANGELOG.md @@ -1,3 +1,5 @@ -# @tanstack/electric-db-collection +# @tanstack/rxdb-db-collection ## 0.0.0 + +- Initial Release diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index 6e1838f0b..0f1ee64de 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -30,7 +30,7 @@ export const OPEN_RXDB_SUBSCRIPTIONS = new WeakMap Date: Fri, 29 Aug 2025 15:16:51 +0200 Subject: [PATCH 08/18] CHANGE use localstorage in example for simpleness --- docs/collections/rxdb-collection.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/collections/rxdb-collection.md b/docs/collections/rxdb-collection.md index 6ed4ae0de..09a9470e5 100644 --- a/docs/collections/rxdb-collection.md +++ b/docs/collections/rxdb-collection.md @@ -31,7 +31,12 @@ npm install @tanstack/rxdb-db-collection rxdb @tanstack/db ```ts import { createRxDatabase, addRxPlugin } from 'rxdb/plugins/core' -import { getRxStorageDexie } from 'rxdb/plugins/storage-dexie' // Browser (IndexedDB via Dexie.js) + +/** + * Here we use the localstorage based storage for RxDB. + * RxDB has a wide range of storages based on Dexie.js, IndexedDB, SQLite and more. + */ +import { getRxStorageLocalstorage } from 'rxdb/plugins/storage-localstorage' // add json-schema validation (optional) import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv'; @@ -45,7 +50,7 @@ type Todo = { id: string; text: string; completed: boolean } const db = await createRxDatabase({ name: 'my-todos', storage: wrappedValidateAjvStorage({ - storage: getRxStorageDexie() + storage: getRxStorageLocalstorage() }) }) From 8afe62f11ecf7bde457384010d931a7c4140c81b Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:43:39 +0200 Subject: [PATCH 09/18] UPDATE pnpm-lock --- pnpm-lock.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5cb31dcbb..f3b163d89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -596,6 +596,34 @@ importers: specifier: ^19.0.0 version: 19.1.1(react@19.1.1) + packages/rxdb-db-collection: + dependencies: + '@standard-schema/spec': + specifier: ^1.0.0 + version: 1.0.0 + '@tanstack/db': + specifier: workspace:* + version: link:../db + '@tanstack/store': + specifier: ^0.7.0 + version: 0.7.2 + debug: + specifier: ^4.4.1 + version: 4.4.1 + rxdb: + specifier: 16.17.2 + version: 16.17.2(rxjs@7.8.2) + typescript: + specifier: '>=4.7' + version: 5.8.3 + devDependencies: + '@types/debug': + specifier: ^4.1.12 + version: 4.1.12 + '@vitest/coverage-istanbul': + specifier: ^3.0.9 + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.0)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + packages/solid-db: dependencies: '@solid-primitives/map': From 5f49832654b6d26d6b11340a8b5b3635dcf3c19a Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:45:10 +0200 Subject: [PATCH 10/18] UPDATE lockfile --- pnpm-lock.yaml | 1214 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1214 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f3b163d89..0e6de8108 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -882,6 +882,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.27.0': + resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} + engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.2': resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} engines: {node: '>=6.9.0'} @@ -1672,9 +1676,228 @@ packages: '@fastify/busboy@3.1.1': resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==} + '@firebase/ai@1.4.1': + resolution: {integrity: sha512-bcusQfA/tHjUjBTnMx6jdoPMpDl3r8K15Z+snHz9wq0Foox0F/V+kNLXucEOHoTL2hTc9l+onZCyBJs2QoIC3g==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-types': 0.x + + '@firebase/analytics-compat@0.2.23': + resolution: {integrity: sha512-3AdO10RN18G5AzREPoFgYhW6vWXr3u+OYQv6pl3CX6Fky8QRk0AHurZlY3Q1xkXO0TDxIsdhO3y65HF7PBOJDw==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/analytics-types@0.8.3': + resolution: {integrity: sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==} + + '@firebase/analytics@0.10.17': + resolution: {integrity: sha512-n5vfBbvzduMou/2cqsnKrIes4auaBjdhg8QNA2ZQZ59QgtO2QiwBaXQZQE4O4sgB0Ds1tvLgUUkY+pwzu6/xEg==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-check-compat@0.3.26': + resolution: {integrity: sha512-PkX+XJMLDea6nmnopzFKlr+s2LMQGqdyT2DHdbx1v1dPSqOol2YzgpgymmhC67vitXVpNvS3m/AiWQWWhhRRPQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/app-check-interop-types@0.3.3': + resolution: {integrity: sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==} + + '@firebase/app-check-types@0.5.3': + resolution: {integrity: sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==} + + '@firebase/app-check@0.10.1': + resolution: {integrity: sha512-MgNdlms9Qb0oSny87pwpjKush9qUwCJhfmTJHDfrcKo4neLGiSeVE4qJkzP7EQTIUFKp84pbTxobSAXkiuQVYQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-compat@0.4.2': + resolution: {integrity: sha512-LssbyKHlwLeiV8GBATyOyjmHcMpX/tFjzRUCS1jnwGAew1VsBB4fJowyS5Ud5LdFbYpJeS+IQoC+RQxpK7eH3Q==} + engines: {node: '>=18.0.0'} + + '@firebase/app-types@0.9.3': + resolution: {integrity: sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==} + + '@firebase/app@0.13.2': + resolution: {integrity: sha512-jwtMmJa1BXXDCiDx1vC6SFN/+HfYG53UkfJa6qeN5ogvOunzbFDO3wISZy5n9xgYFUrEP6M7e8EG++riHNTv9w==} + engines: {node: '>=18.0.0'} + + '@firebase/auth-compat@0.5.28': + resolution: {integrity: sha512-HpMSo/cc6Y8IX7bkRIaPPqT//Jt83iWy5rmDWeThXQCAImstkdNo3giFLORJwrZw2ptiGkOij64EH1ztNJzc7Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/auth-interop-types@0.2.4': + resolution: {integrity: sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==} + + '@firebase/auth-types@0.13.0': + resolution: {integrity: sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/auth@1.10.8': + resolution: {integrity: sha512-GpuTz5ap8zumr/ocnPY57ZanX02COsXloY6Y/2LYPAuXYiaJRf6BAGDEdRq1BMjP93kqQnKNuKZUTMZbQ8MNYA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@react-native-async-storage/async-storage': ^1.18.1 + peerDependenciesMeta: + '@react-native-async-storage/async-storage': + optional: true + + '@firebase/component@0.6.18': + resolution: {integrity: sha512-n28kPCkE2dL2U28fSxZJjzPPVpKsQminJ6NrzcKXAI0E/lYC8YhfwpyllScqVEvAI3J2QgJZWYgrX+1qGI+SQQ==} + engines: {node: '>=18.0.0'} + + '@firebase/data-connect@0.3.10': + resolution: {integrity: sha512-VMVk7zxIkgwlVQIWHOKFahmleIjiVFwFOjmakXPd/LDgaB/5vzwsB5DWIYo+3KhGxWpidQlR8geCIn39YflJIQ==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/database-compat@2.0.11': + resolution: {integrity: sha512-itEsHARSsYS95+udF/TtIzNeQ0Uhx4uIna0sk4E0wQJBUnLc/G1X6D7oRljoOuwwCezRLGvWBRyNrugv/esOEw==} + engines: {node: '>=18.0.0'} + + '@firebase/database-types@1.0.15': + resolution: {integrity: sha512-XWHJ0VUJ0k2E9HDMlKxlgy/ZuTa9EvHCGLjaKSUvrQnwhgZuRU5N3yX6SZ+ftf2hTzZmfRkv+b3QRvGg40bKNw==} + + '@firebase/database@1.0.20': + resolution: {integrity: sha512-H9Rpj1pQ1yc9+4HQOotFGLxqAXwOzCHsRSRjcQFNOr8lhUt6LeYjf0NSRL04sc4X0dWe8DsCvYKxMYvFG/iOJw==} + engines: {node: '>=18.0.0'} + + '@firebase/firestore-compat@0.3.53': + resolution: {integrity: sha512-qI3yZL8ljwAYWrTousWYbemay2YZa+udLWugjdjju2KODWtLG94DfO4NALJgPLv8CVGcDHNFXoyQexdRA0Cz8Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/firestore-types@3.0.3': + resolution: {integrity: sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/firestore@4.8.0': + resolution: {integrity: sha512-QSRk+Q1/CaabKyqn3C32KSFiOdZpSqI9rpLK5BHPcooElumOBooPFa6YkDdiT+/KhJtel36LdAacha9BptMj2A==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/functions-compat@0.3.26': + resolution: {integrity: sha512-A798/6ff5LcG2LTWqaGazbFYnjBW8zc65YfID/en83ALmkhu2b0G8ykvQnLtakbV9ajrMYPn7Yc/XcYsZIUsjA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/functions-types@0.6.3': + resolution: {integrity: sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==} + + '@firebase/functions@0.12.9': + resolution: {integrity: sha512-FG95w6vjbUXN84Ehezc2SDjGmGq225UYbHrb/ptkRT7OTuCiQRErOQuyt1jI1tvcDekdNog+anIObihNFz79Lg==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/installations-compat@0.2.18': + resolution: {integrity: sha512-aLFohRpJO5kKBL/XYL4tN+GdwEB/Q6Vo9eZOM/6Kic7asSUgmSfGPpGUZO1OAaSRGwF4Lqnvi1f/f9VZnKzChw==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/installations-types@0.5.3': + resolution: {integrity: sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==} + peerDependencies: + '@firebase/app-types': 0.x + + '@firebase/installations@0.6.18': + resolution: {integrity: sha512-NQ86uGAcvO8nBRwVltRL9QQ4Reidc/3whdAasgeWCPIcrhOKDuNpAALa6eCVryLnK14ua2DqekCOX5uC9XbU/A==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/logger@0.4.4': + resolution: {integrity: sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==} + engines: {node: '>=18.0.0'} + + '@firebase/messaging-compat@0.2.22': + resolution: {integrity: sha512-5ZHtRnj6YO6f/QPa/KU6gryjmX4Kg33Kn4gRpNU6M1K47Gm8kcQwPkX7erRUYEH1mIWptfvjvXMHWoZaWjkU7A==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/messaging-interop-types@0.2.3': + resolution: {integrity: sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==} + + '@firebase/messaging@0.12.22': + resolution: {integrity: sha512-GJcrPLc+Hu7nk+XQ70Okt3M1u1eRr2ZvpMbzbc54oTPJZySHcX9ccZGVFcsZbSZ6o1uqumm8Oc7OFkD3Rn1/og==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/performance-compat@0.2.20': + resolution: {integrity: sha512-XkFK5NmOKCBuqOKWeRgBUFZZGz9SzdTZp4OqeUg+5nyjapTiZ4XoiiUL8z7mB2q+63rPmBl7msv682J3rcDXIQ==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/performance-types@0.2.3': + resolution: {integrity: sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==} + + '@firebase/performance@0.7.7': + resolution: {integrity: sha512-JTlTQNZKAd4+Q5sodpw6CN+6NmwbY72av3Lb6wUKTsL7rb3cuBIhQSrslWbVz0SwK3x0ZNcqX24qtRbwKiv+6w==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/remote-config-compat@0.2.18': + resolution: {integrity: sha512-YiETpldhDy7zUrnS8e+3l7cNs0sL7+tVAxvVYU0lu7O+qLHbmdtAxmgY+wJqWdW2c9nDvBFec7QiF58pEUu0qQ==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/remote-config-types@0.4.0': + resolution: {integrity: sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg==} + + '@firebase/remote-config@0.6.5': + resolution: {integrity: sha512-fU0c8HY0vrVHwC+zQ/fpXSqHyDMuuuglV94VF6Yonhz8Fg2J+KOowPGANM0SZkLvVOYpTeWp3ZmM+F6NjwWLnw==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/storage-compat@0.3.24': + resolution: {integrity: sha512-XHn2tLniiP7BFKJaPZ0P8YQXKiVJX+bMyE2j2YWjYfaddqiJnROJYqSomwW6L3Y+gZAga35ONXUJQju6MB6SOQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/storage-types@0.8.3': + resolution: {integrity: sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/storage@0.13.14': + resolution: {integrity: sha512-xTq5ixxORzx+bfqCpsh+o3fxOsGoDjC1nO0Mq2+KsOcny3l7beyBhP/y1u5T6mgsFQwI1j6oAkbT5cWdDBx87g==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/util@1.12.1': + resolution: {integrity: sha512-zGlBn/9Dnya5ta9bX/fgEoNC3Cp8s6h+uYPYaDieZsFOAdHP/ExzQ/eaDgxD3GOROdPkLKpvKY0iIzr9adle0w==} + engines: {node: '>=18.0.0'} + + '@firebase/webchannel-wrapper@1.0.3': + resolution: {integrity: sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==} + '@gerrit0/mini-shiki@1.27.2': resolution: {integrity: sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og==} + '@grpc/grpc-js@1.9.15': + resolution: {integrity: sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==} + engines: {node: ^8.13.0 || >=10.10.0} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + '@hexagon/base64@1.1.28': resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} @@ -1773,6 +1996,9 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@mongodb-js/saslprep@1.3.0': + resolution: {integrity: sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -1973,6 +2199,36 @@ packages: '@poppinss/exception@1.2.2': resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@publint/pack@0.1.2': resolution: {integrity: sha512-S+9ANAvUmjutrshV4jZjaiG8XQyuJIZ8a4utWmN/vW1sgQ9IfBnPndwkmQYw53QmouOIytT874u65HEmu6H5jw==} engines: {node: '>=18'} @@ -2763,6 +3019,12 @@ packages: '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/clone@2.1.4': + resolution: {integrity: sha512-NKRWaEGaVGVLnGLB2GazvDaZnyweW9FJLLFL5LhywGJB3aqGMT9R/EUoJoSRP4nzofYnZysuDmrEJtJdAqUOtQ==} + + '@types/common-tags@1.8.1': + resolution: {integrity: sha512-20R/mDpKSPWdJs5TOpz3e7zqbeCNuMCPhV7Yndk9KU2Rbij2r5W4RzwDPkzC+2lzUqXYu9rFzTktCBnDjHuNQg==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -2784,15 +3046,24 @@ packages: '@types/express-serve-static-core@4.19.6': resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + '@types/express-serve-static-core@5.0.7': + resolution: {integrity: sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==} + '@types/express@4.17.23': resolution: {integrity: sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==} + '@types/express@5.0.3': + resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/json-schema@7.0.11': + resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -2840,6 +3111,9 @@ packages: '@types/serve-static@1.15.8': resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} + '@types/simple-peer@9.11.8': + resolution: {integrity: sha512-rvqefdp2rvIA6wiomMgKWd2UZNPe6LM2EV5AuY3CPQJF+8TbdrL5TjYdMf0VAjGczzlkH4l1NjDkihwbj3Xodw==} + '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} @@ -2849,6 +3123,15 @@ packages: '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/webidl-conversions@7.0.3': + resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + + '@types/whatwg-url@11.0.5': + resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -3187,6 +3470,9 @@ packages: ajv@8.13.0: resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -3264,6 +3550,9 @@ packages: resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} engines: {node: '>= 0.4'} + array-push-at-sort-position@4.0.1: + resolution: {integrity: sha512-KdtdxZmp+j6n+jiekMbBRO/TOVP7oEadrJ+M4jB0Oe1VHZHS1Uwzx5lsvFN4juNZtHNA1l1fvcEs/SDmdoXL3w==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -3288,6 +3577,9 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + as-typed@1.3.2: + resolution: {integrity: sha512-94ezeKlKB97OJUyMaU7dQUAB+Cmjlgr4T9/cxCoKaLM4F2HAmuIHm3Q5ClGCsX5PvRwCQehCzAa/6foRFXRbqA==} + asn1js@3.0.6: resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==} engines: {node: '>=12.0.0'} @@ -3365,6 +3657,10 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + binary-decision-diagram@3.2.0: + resolution: {integrity: sha512-Pu9LnLdNIpUI6nSSTSJW1IlmTmPVMCJHqr/atIigdeJYTDAI/198AvnAbxuSrCxiJLoTCNiPBzdpHEJMjOZiAQ==} + engines: {node: '>=16'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -3389,11 +3685,18 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + broadcast-channel@7.1.0: + resolution: {integrity: sha512-InJljddsYWbEL8LBnopnCg+qMQp9KcowvYWOt4YWrjD5HmxzDYKdVbDS1w/ji5rFZdRD58V5UxJPtBdpEbEJYw==} + browserslist@4.25.1: resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bson@6.10.4: + resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} + engines: {node: '>=16.20.1'} + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -3553,6 +3856,10 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -3575,6 +3882,10 @@ packages: common-path-prefix@3.0.0: resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -3688,6 +3999,9 @@ packages: crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} @@ -3705,6 +4019,9 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + custom-idle-queue@4.1.0: + resolution: {integrity: sha512-/7Qe5ZRrZllm/XCV+w7OfaRG/SJxnB94BnaA78jk/bbHXhfUPSqu07c6UGd3tg2LKqV+5ju/dnEI1xAgZpNRGA==} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -3791,6 +4108,10 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + defekt@9.3.0: + resolution: {integrity: sha512-AWfM0vhFmESRZawEJfLhRJMsAR5IOhwyxGxIDOh9RXGKcdV65cWtkFB31MNjUfFvAlfbk3c2ooX0rr1pWIXshw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -3881,6 +4202,9 @@ packages: peerDependencies: typescript: ^5.4.4 + dexie@4.0.10: + resolution: {integrity: sha512-eM2RzuR3i+M046r2Q0Optl3pS31qTWf8aFuA7H9wnsHTwl8EPvroVLwvQene/6paAs39Tbk6fWZcn2aZaHkc/w==} + diff@8.0.2: resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} engines: {node: '>=0.3.1'} @@ -4195,6 +4519,9 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + err-code@3.0.1: + resolution: {integrity: sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==} + error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} @@ -4434,10 +4761,17 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-reduce-js@5.2.7: + resolution: {integrity: sha512-Vi6aIiAmakzx81JAwhw8L988aSX5a3ZqqVjHyZa9xFU6P4oT1IotoDreWtjNlS+fvEnASvyIQT565nmkOtns/Q==} + engines: {node: '>=16'} + event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -4495,9 +4829,16 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -4551,6 +4892,9 @@ packages: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} + firebase@11.10.0: + resolution: {integrity: sha512-nKBXoDzF0DrXTBQJlZa+sbC5By99ysYU1D6PkMRYknm0nCW7rJly47q492Ht7Ndz5MeYSBuboKuhS1e6mFC03w==} + fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} @@ -4620,6 +4964,12 @@ packages: engines: {node: '>= 18.0.0'} hasBin: true + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + + generate-object-property@1.2.0: + resolution: {integrity: sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -4628,6 +4978,9 @@ packages: resolution: {integrity: sha512-MtjsmYiCXcYDDrGqtNbeIYdAl85n+5mSv2r3FbzER/YV3ZILw4HNNIw34HuV5pyl0jzs6GFYU1VHVEefhgcNHQ==} engines: {node: '>=18'} + get-browser-rtc@1.1.0: + resolution: {integrity: sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -4636,6 +4989,10 @@ packages: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} + get-graphql-from-jsonschema@8.1.0: + resolution: {integrity: sha512-MhvxGPBjJm1ls6XmvcmgJG7ApqxkFEs5T8uDzytlpbMBBwMMnoF/rMUWzPxM6YvejyLhCB3axD4Dwci3G5F4UA==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -4729,6 +5086,16 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql-ws@5.16.2: + resolution: {integrity: sha512-E1uccsZxt/96jH/OwmLPuXMACILs76pKF2i3W861LpKBCYtGIyPQGtWLuBLkND4ox1KHns70e83PS4te50nvPQ==} + engines: {node: '>=10'} + peerDependencies: + graphql: '>=0.11 <=16' + + graphql@15.10.1: + resolution: {integrity: sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==} + engines: {node: '>= 10.x'} + gzip-size@7.0.0: resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4801,6 +5168,9 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -4837,6 +5207,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -4893,6 +5266,10 @@ packages: iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -4990,6 +5367,12 @@ packages: is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + is-my-ip-valid@1.0.1: + resolution: {integrity: sha512-jxc8cBcOWbNK2i2aTkCZP6i7wkHF1bqKFrwEHuN5Jtg5BSaZHUZQ/JTOJwoV41YvHnOaRyWWh72T/KvfNz9DJg==} + + is-my-json-valid@2.20.6: + resolution: {integrity: sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==} + is-negative-zero@2.0.3: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} @@ -5017,6 +5400,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -5127,6 +5513,11 @@ packages: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -5168,6 +5559,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -5227,6 +5621,13 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + + jsonschema-key-compression@1.7.0: + resolution: {integrity: sha512-l3RxhqT+IIp7He/BQ6Ao9PvK2rOa0sJse1ZoaJIKpiY1RC9Sy4GRhweDtxRGnQe8kuPOedTIE5Dq36fSYCFwnA==} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -5399,15 +5800,26 @@ packages: lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -5428,6 +5840,9 @@ packages: resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -5492,6 +5907,9 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + meow@12.1.1: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} @@ -5568,6 +5986,9 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + mingo@6.5.6: + resolution: {integrity: sha512-XV89xbTakngi/oIEpuq7+FXXYvdA/Ht6aAsNTuIl8zLW1jfv369Va1PPWod1UTa/cqL0pC6LD2P6ggBcSSeH+A==} + minimatch@10.0.3: resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} engines: {node: 20 || >=22} @@ -5613,6 +6034,36 @@ packages: engines: {node: '>=18'} hasBin: true + mongodb-connection-string-url@3.0.2: + resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} + + mongodb@6.18.0: + resolution: {integrity: sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==} + engines: {node: '>=16.20.1'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.188.0 + '@mongodb-js/zstd': ^1.1.0 || ^2.0.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' + snappy: ^7.2.2 + socks: ^2.7.1 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -5646,6 +6097,10 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true + nats@2.29.3: + resolution: {integrity: sha512-tOQCRCwC74DgBTk4pWZ9V45sk4d7peoE2njVprMRCBXrhJ5q5cYM7i6W+Uvw2qUrcfOSnuisrX7bEx3b3Wx4QA==} + engines: {node: '>= 14.0.0'} + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -5670,6 +6125,10 @@ packages: xml2js: optional: true + nkeys.js@1.1.0: + resolution: {integrity: sha512-tB/a0shZL5UZWSwsoeyqfTszONTt4k2YS0tuQioMOD180+MbombYVgzDUYHlx+gejYK6rgf08n/2Df99WY0Sxg==} + engines: {node: '>=10.0.0'} + no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -5779,6 +6238,10 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + oblivious-set@1.4.0: + resolution: {integrity: sha512-szyd0ou0T8nsAqHtprRcP3WidfsN1TnAR5yWXf2mFCEr5ek3LEOkT6EZ/92Xfs74HIdyhG5WkGxIssMU0jBaeg==} + engines: {node: '>=16'} + ofetch@1.4.1: resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} @@ -5869,6 +6332,14 @@ packages: resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==} engines: {node: '>=18'} + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + p-timeout@6.1.4: resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} engines: {node: '>=14.16'} @@ -6135,6 +6606,10 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -6257,6 +6732,9 @@ packages: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} engines: {node: '>= 0.10'} + reconnecting-websocket@4.4.0: + resolution: {integrity: sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -6273,6 +6751,9 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -6354,6 +6835,12 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxdb@16.17.2: + resolution: {integrity: sha512-KGHP2ZWhL9ZQAkFZqaX1+G6Im3yx4WFudS9T6soN1ems49rG2koiLAkr160tcOCJRH4wt1rNjd0/vvW6YFyPYw==} + engines: {node: '>=18'} + peerDependencies: + rxjs: ^7.8.0 + rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} @@ -6523,6 +7010,9 @@ packages: simple-git@3.28.0: resolution: {integrity: sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==} + simple-peer@9.11.1: + resolution: {integrity: sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==} + simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} @@ -6576,6 +7066,9 @@ packages: engines: {node: '>= 8'} deprecated: The work that was done in this beta branch won't be included in future versions + sparse-bitfield@3.0.3: + resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} @@ -6943,6 +7436,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -7062,6 +7558,9 @@ packages: resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==} engines: {node: '>=0.10.0'} + unload@2.4.1: + resolution: {integrity: sha512-IViSAm8Z3sRBYA+9wc0fLQmU9Nrxb16rcDmIiR6Y9LJSZzI7QY5QsDhqPpKOjAn0O9/kfK1TfNEMMAGPTIraPw==} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -7177,6 +7676,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -7191,6 +7693,10 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + validator@13.15.15: + resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==} + engines: {node: '>= 0.10'} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -7334,6 +7840,9 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + web-vitals@5.0.3: resolution: {integrity: sha512-4KmOFYxj7qT6RAdCH0SWwq8eKeXNhAFXR4PmgF6nrWFmrJ41n7lq3UCA6UK0GebQ4uu+XP8e8zGjaDO3wZlqTg==} @@ -7350,6 +7859,14 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -7510,6 +8027,11 @@ packages: resolution: {integrity: sha512-rY2A2lSF7zC+l7HH9Mq+83D1dLlsPnEvy8jTouzaptDZM6geqZ3aJe/b7ULCwRURPtWV3vbDjA2DDMdoBol0HQ==} engines: {node: '>=18'} + z-schema@6.0.2: + resolution: {integrity: sha512-9fQb2ZhpMD0ZQXYw0ll5ya6uLQm3Xtt4DXY2RV3QO1QVI4ihSzSWirlgkDsMgGg4qK0EV4tLOJgRSH2bn0cbIw==} + engines: {node: '>=16.0.0'} + hasBin: true + zimmerframe@1.1.2: resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} @@ -7732,6 +8254,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/runtime@7.27.0': + dependencies: + regenerator-runtime: 0.14.1 + '@babel/runtime@7.28.2': {} '@babel/template@7.27.2': @@ -8348,12 +8874,342 @@ snapshots: '@fastify/busboy@3.1.1': {} + '@firebase/ai@1.4.1(@firebase/app-types@0.9.3)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/app-types': 0.9.3 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/analytics-compat@0.2.23(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2)': + dependencies: + '@firebase/analytics': 0.10.17(@firebase/app@0.13.2) + '@firebase/analytics-types': 0.8.3 + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/analytics-types@0.8.3': {} + + '@firebase/analytics@0.10.17(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/installations': 0.6.18(@firebase/app@0.13.2) + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/app-check-compat@0.3.26(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-check': 0.10.1(@firebase/app@0.13.2) + '@firebase/app-check-types': 0.5.3 + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/app-check-interop-types@0.3.3': {} + + '@firebase/app-check-types@0.5.3': {} + + '@firebase/app-check@0.10.1(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/app-compat@0.4.2': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/app-types@0.9.3': {} + + '@firebase/app@0.13.2': + dependencies: + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/auth-compat@0.5.28(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/auth': 1.10.8(@firebase/app@0.13.2) + '@firebase/auth-types': 0.13.0(@firebase/app-types@0.9.3)(@firebase/util@1.12.1) + '@firebase/component': 0.6.18 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + - '@react-native-async-storage/async-storage' + + '@firebase/auth-interop-types@0.2.4': {} + + '@firebase/auth-types@0.13.0(@firebase/app-types@0.9.3)(@firebase/util@1.12.1)': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.1 + + '@firebase/auth@1.10.8(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/component@0.6.18': + dependencies: + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/data-connect@0.3.10(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/database-compat@2.0.11': + dependencies: + '@firebase/component': 0.6.18 + '@firebase/database': 1.0.20 + '@firebase/database-types': 1.0.15 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/database-types@1.0.15': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.1 + + '@firebase/database@1.0.20': + dependencies: + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + faye-websocket: 0.11.4 + tslib: 2.8.1 + + '@firebase/firestore-compat@0.3.53(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/firestore': 4.8.0(@firebase/app@0.13.2) + '@firebase/firestore-types': 3.0.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.1) + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/firestore-types@3.0.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.1)': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.1 + + '@firebase/firestore@4.8.0(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + '@firebase/webchannel-wrapper': 1.0.3 + '@grpc/grpc-js': 1.9.15 + '@grpc/proto-loader': 0.7.15 + tslib: 2.8.1 + + '@firebase/functions-compat@0.3.26(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/functions': 0.12.9(@firebase/app@0.13.2) + '@firebase/functions-types': 0.6.3 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/functions-types@0.6.3': {} + + '@firebase/functions@0.12.9(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.18 + '@firebase/messaging-interop-types': 0.2.3 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/installations-compat@0.2.18(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/installations': 0.6.18(@firebase/app@0.13.2) + '@firebase/installations-types': 0.5.3(@firebase/app-types@0.9.3) + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/installations-types@0.5.3(@firebase/app-types@0.9.3)': + dependencies: + '@firebase/app-types': 0.9.3 + + '@firebase/installations@0.6.18(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/util': 1.12.1 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/logger@0.4.4': + dependencies: + tslib: 2.8.1 + + '@firebase/messaging-compat@0.2.22(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/messaging': 0.12.22(@firebase/app@0.13.2) + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/messaging-interop-types@0.2.3': {} + + '@firebase/messaging@0.12.22(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/installations': 0.6.18(@firebase/app@0.13.2) + '@firebase/messaging-interop-types': 0.2.3 + '@firebase/util': 1.12.1 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/performance-compat@0.2.20(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/performance': 0.7.7(@firebase/app@0.13.2) + '@firebase/performance-types': 0.2.3 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/performance-types@0.2.3': {} + + '@firebase/performance@0.7.7(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/installations': 0.6.18(@firebase/app@0.13.2) + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + web-vitals: 4.2.4 + + '@firebase/remote-config-compat@0.2.18(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/remote-config': 0.6.5(@firebase/app@0.13.2) + '@firebase/remote-config-types': 0.4.0 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/remote-config-types@0.4.0': {} + + '@firebase/remote-config@0.6.5(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/installations': 0.6.18(@firebase/app@0.13.2) + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/storage-compat@0.3.24(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/storage': 0.13.14(@firebase/app@0.13.2) + '@firebase/storage-types': 0.8.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.1) + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/storage-types@0.8.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.1)': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.1 + + '@firebase/storage@0.13.14(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/util@1.12.1': + dependencies: + tslib: 2.8.1 + + '@firebase/webchannel-wrapper@1.0.3': {} + '@gerrit0/mini-shiki@1.27.2': dependencies: '@shikijs/engine-oniguruma': 1.29.2 '@shikijs/types': 1.29.2 '@shikijs/vscode-textmate': 10.0.2 + '@grpc/grpc-js@1.9.15': + dependencies: + '@grpc/proto-loader': 0.7.15 + '@types/node': 22.17.0 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + '@hexagon/base64@1.1.28': {} '@humanfs/core@0.19.1': {} @@ -8487,6 +9343,10 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} + '@mongodb-js/saslprep@1.3.0': + dependencies: + sparse-bitfield: 3.0.3 + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.4.5 @@ -8734,6 +9594,29 @@ snapshots: '@poppinss/exception@1.2.2': {} + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@publint/pack@0.1.2': {} '@rolldown/pluginutils@1.0.0-beta.27': {} @@ -9994,6 +10877,10 @@ snapshots: dependencies: '@types/deep-eql': 4.0.2 + '@types/clone@2.1.4': {} + + '@types/common-tags@1.8.1': {} + '@types/connect@3.4.38': dependencies: '@types/node': 22.17.0 @@ -10021,6 +10908,13 @@ snapshots: '@types/range-parser': 1.2.7 '@types/send': 0.17.5 + '@types/express-serve-static-core@5.0.7': + dependencies: + '@types/node': 22.17.0 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.5 + '@types/express@4.17.23': dependencies: '@types/body-parser': 1.19.6 @@ -10028,12 +10922,20 @@ snapshots: '@types/qs': 6.14.0 '@types/serve-static': 1.15.8 + '@types/express@5.0.3': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.0.7 + '@types/serve-static': 1.15.8 + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 '@types/http-errors@2.0.5': {} + '@types/json-schema@7.0.11': {} + '@types/json-schema@7.0.15': {} '@types/mime@1.3.5': {} @@ -10081,12 +10983,26 @@ snapshots: '@types/node': 22.17.0 '@types/send': 0.17.5 + '@types/simple-peer@9.11.8': + dependencies: + '@types/node': 22.17.0 + '@types/triple-beam@1.3.5': {} '@types/unist@3.0.3': {} '@types/use-sync-external-store@0.0.6': {} + '@types/webidl-conversions@7.0.3': {} + + '@types/whatwg-url@11.0.5': + dependencies: + '@types/webidl-conversions': 7.0.3 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.17.0 + '@types/yauzl@2.10.3': dependencies: '@types/node': 22.17.0 @@ -10486,6 +11402,10 @@ snapshots: optionalDependencies: ajv: 8.13.0 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -10507,6 +11427,13 @@ snapshots: require-from-string: 2.0.2 uri-js: 4.4.1 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-colors@4.1.3: {} ansi-escapes@7.0.0: @@ -10591,6 +11518,8 @@ snapshots: is-string: 1.1.1 math-intrinsics: 1.1.0 + array-push-at-sort-position@4.0.1: {} + array-union@2.1.0: {} array.prototype.findlast@1.2.5: @@ -10634,6 +11563,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + as-typed@1.3.2: {} + asn1js@3.0.6: dependencies: pvtsutils: 1.3.6 @@ -10722,6 +11653,8 @@ snapshots: dependencies: is-windows: 1.0.2 + binary-decision-diagram@3.2.0: {} + binary-extensions@2.3.0: {} bindings@1.5.0: @@ -10760,6 +11693,13 @@ snapshots: dependencies: fill-range: 7.1.1 + broadcast-channel@7.1.0: + dependencies: + '@babel/runtime': 7.27.0 + oblivious-set: 1.4.0 + p-queue: 6.6.2 + unload: 2.4.1 + browserslist@4.25.1: dependencies: caniuse-lite: 1.0.30001731 @@ -10767,6 +11707,8 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.1) + bson@6.10.4: {} + buffer-crc32@0.2.13: {} buffer-crc32@1.0.0: {} @@ -10951,6 +11893,9 @@ snapshots: commander@10.0.1: {} + commander@11.1.0: + optional: true + commander@12.1.0: {} commander@13.1.0: {} @@ -10963,6 +11908,8 @@ snapshots: common-path-prefix@3.0.0: {} + common-tags@1.8.2: {} + commondir@1.0.1: {} compare-func@2.0.0: @@ -11074,6 +12021,8 @@ snapshots: dependencies: uncrypto: 0.1.3 + crypto-js@4.2.0: {} + css-select@5.2.2: dependencies: boolbase: 1.0.0 @@ -11093,6 +12042,8 @@ snapshots: csstype@3.1.3: {} + custom-idle-queue@4.1.0: {} + data-uri-to-buffer@4.0.1: {} data-urls@5.0.0: @@ -11152,6 +12103,8 @@ snapshots: deepmerge@4.3.1: {} + defekt@9.3.0: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -11240,6 +12193,8 @@ snapshots: transitivePeerDependencies: - supports-color + dexie@4.0.10: {} + diff@8.0.2: {} dir-glob@3.0.1: @@ -11387,6 +12342,8 @@ snapshots: environment@1.1.0: {} + err-code@3.0.1: {} + error-stack-parser-es@1.0.5: {} es-abstract@1.24.0: @@ -11838,8 +12795,15 @@ snapshots: etag@1.8.1: {} + event-reduce-js@5.2.7: + dependencies: + array-push-at-sort-position: 4.0.1 + binary-decision-diagram: 3.2.0 + event-target-shim@5.0.1: {} + eventemitter3@4.0.7: {} + eventemitter3@5.0.1: {} events@3.3.0: {} @@ -11942,10 +12906,16 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + fd-slicer@1.1.0: dependencies: pend: 1.2.0 @@ -12003,6 +12973,39 @@ snapshots: path-exists: 5.0.0 unicorn-magic: 0.1.0 + firebase@11.10.0: + dependencies: + '@firebase/ai': 1.4.1(@firebase/app-types@0.9.3)(@firebase/app@0.13.2) + '@firebase/analytics': 0.10.17(@firebase/app@0.13.2) + '@firebase/analytics-compat': 0.2.23(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2) + '@firebase/app': 0.13.2 + '@firebase/app-check': 0.10.1(@firebase/app@0.13.2) + '@firebase/app-check-compat': 0.3.26(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2) + '@firebase/app-compat': 0.4.2 + '@firebase/app-types': 0.9.3 + '@firebase/auth': 1.10.8(@firebase/app@0.13.2) + '@firebase/auth-compat': 0.5.28(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2) + '@firebase/data-connect': 0.3.10(@firebase/app@0.13.2) + '@firebase/database': 1.0.20 + '@firebase/database-compat': 2.0.11 + '@firebase/firestore': 4.8.0(@firebase/app@0.13.2) + '@firebase/firestore-compat': 0.3.53(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2) + '@firebase/functions': 0.12.9(@firebase/app@0.13.2) + '@firebase/functions-compat': 0.3.26(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2) + '@firebase/installations': 0.6.18(@firebase/app@0.13.2) + '@firebase/installations-compat': 0.2.18(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2) + '@firebase/messaging': 0.12.22(@firebase/app@0.13.2) + '@firebase/messaging-compat': 0.2.22(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2) + '@firebase/performance': 0.7.7(@firebase/app@0.13.2) + '@firebase/performance-compat': 0.2.20(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2) + '@firebase/remote-config': 0.6.5(@firebase/app@0.13.2) + '@firebase/remote-config-compat': 0.2.18(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2) + '@firebase/storage': 0.13.14(@firebase/app@0.13.2) + '@firebase/storage-compat': 0.3.24(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2) + '@firebase/util': 1.12.1 + transitivePeerDependencies: + - '@react-native-async-storage/async-storage' + fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.17 @@ -12078,6 +13081,14 @@ snapshots: transitivePeerDependencies: - supports-color + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + + generate-object-property@1.2.0: + dependencies: + is-property: 1.0.2 + gensync@1.0.0-beta.2: {} get-amd-module-type@6.0.1: @@ -12085,10 +13096,19 @@ snapshots: ast-module-types: 6.0.1 node-source-walk: 7.0.1 + get-browser-rtc@1.1.0: {} + get-caller-file@2.0.5: {} get-east-asian-width@1.3.0: {} + get-graphql-from-jsonschema@8.1.0: + dependencies: + '@types/common-tags': 1.8.1 + '@types/json-schema': 7.0.11 + common-tags: 1.8.2 + defekt: 9.3.0 + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -12200,6 +13220,12 @@ snapshots: graphemer@1.4.0: {} + graphql-ws@5.16.2(graphql@15.10.1): + dependencies: + graphql: 15.10.1 + + graphql@15.10.1: {} + gzip-size@7.0.0: dependencies: duplexer: 0.1.2 @@ -12288,6 +13314,8 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-parser-js@0.5.10: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -12320,6 +13348,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + idb@7.1.1: {} + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -12369,6 +13399,11 @@ snapshots: iron-webcrypto@1.2.1: {} + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -12460,6 +13495,16 @@ snapshots: is-module@1.0.0: {} + is-my-ip-valid@1.0.1: {} + + is-my-json-valid@2.20.6: + dependencies: + generate-function: 2.3.1 + generate-object-property: 1.2.0 + is-my-ip-valid: 1.0.1 + jsonpointer: 5.0.1 + xtend: 4.0.2 + is-negative-zero@2.0.3: {} is-number-object@1.1.1: @@ -12477,6 +13522,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-property@1.0.2: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.8 @@ -12570,6 +13617,10 @@ snapshots: isexe@3.1.1: {} + isomorphic-ws@5.0.0(ws@8.18.3): + dependencies: + ws: 8.18.3 + istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@6.0.3: @@ -12624,6 +13675,8 @@ snapshots: joycon@3.1.1: {} + js-base64@3.7.8: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -12695,6 +13748,10 @@ snapshots: jsonparse@1.3.1: {} + jsonpointer@5.0.1: {} + + jsonschema-key-compression@1.7.0: {} + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -12868,12 +13925,18 @@ snapshots: lodash-es@4.17.21: {} + lodash.camelcase@4.3.0: {} + lodash.debounce@4.0.8: {} lodash.defaults@4.2.0: {} + lodash.get@4.4.2: {} + lodash.isarguments@3.1.0: {} + lodash.isequal@4.5.0: {} + lodash.merge@4.6.2: {} lodash.sortby@4.7.0: {} @@ -12899,6 +13962,8 @@ snapshots: safe-stable-stringify: 2.5.0 triple-beam: 1.4.1 + long@5.3.2: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -12961,6 +14026,8 @@ snapshots: media-typer@0.3.0: {} + memory-pager@1.5.0: {} + meow@12.1.1: {} merge-anything@5.1.7: @@ -13010,6 +14077,8 @@ snapshots: min-indent@1.0.1: {} + mingo@6.5.6: {} + minimatch@10.0.3: dependencies: '@isaacs/brace-expansion': 5.0.0 @@ -13054,6 +14123,17 @@ snapshots: ast-module-types: 6.0.1 node-source-walk: 7.0.1 + mongodb-connection-string-url@3.0.2: + dependencies: + '@types/whatwg-url': 11.0.5 + whatwg-url: 14.2.0 + + mongodb@6.18.0: + dependencies: + '@mongodb-js/saslprep': 1.3.0 + bson: 6.10.4 + mongodb-connection-string-url: 3.0.2 + mri@1.2.0: {} ms@2.0.0: {} @@ -13076,6 +14156,10 @@ snapshots: napi-postinstall@0.3.2: {} + nats@2.29.3: + dependencies: + nkeys.js: 1.1.0 + natural-compare@1.4.0: {} negotiator@0.6.3: {} @@ -13291,6 +14375,10 @@ snapshots: - supports-color - uploadthing + nkeys.js@1.1.0: + dependencies: + tweetnacl: 1.0.3 + no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -13398,6 +14486,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + oblivious-set@1.4.0: {} + ofetch@1.4.1: dependencies: destr: 2.0.5 @@ -13491,6 +14581,15 @@ snapshots: p-map@7.0.3: {} + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + p-timeout@6.1.4: {} p-try@2.2.0: {} @@ -13721,6 +14820,21 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 22.17.0 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -13862,6 +14976,8 @@ snapshots: dependencies: resolve: 1.22.10 + reconnecting-websocket@4.4.0: {} + redent@3.0.0: dependencies: indent-string: 4.0.0 @@ -13884,6 +15000,8 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regenerator-runtime@0.14.1: {} + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -13977,6 +15095,54 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rxdb@16.17.2(rxjs@7.8.2): + dependencies: + '@babel/runtime': 7.28.2 + '@types/clone': 2.1.4 + '@types/cors': 2.8.19 + '@types/express': 5.0.3 + '@types/simple-peer': 9.11.8 + '@types/ws': 8.18.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + array-push-at-sort-position: 4.0.1 + as-typed: 1.3.2 + broadcast-channel: 7.1.0 + crypto-js: 4.2.0 + custom-idle-queue: 4.1.0 + dexie: 4.0.10 + event-reduce-js: 5.2.7 + firebase: 11.10.0 + get-graphql-from-jsonschema: 8.1.0 + graphql: 15.10.1 + graphql-ws: 5.16.2(graphql@15.10.1) + is-my-json-valid: 2.20.6 + isomorphic-ws: 5.0.0(ws@8.18.3) + js-base64: 3.7.8 + jsonschema-key-compression: 1.7.0 + mingo: 6.5.6 + mongodb: 6.18.0 + nats: 2.29.3 + oblivious-set: 1.4.0 + reconnecting-websocket: 4.4.0 + rxjs: 7.8.2 + simple-peer: 9.11.1 + util: 0.12.5 + ws: 8.18.3 + z-schema: 6.0.2 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - '@react-native-async-storage/async-storage' + - bufferutil + - gcp-metadata + - kerberos + - mongodb-client-encryption + - snappy + - socks + - supports-color + - utf-8-validate + rxjs@7.8.2: dependencies: tslib: 2.8.1 @@ -14190,6 +15356,18 @@ snapshots: transitivePeerDependencies: - supports-color + simple-peer@9.11.1: + dependencies: + buffer: 6.0.3 + debug: 4.4.1 + err-code: 3.0.1 + get-browser-rtc: 1.1.0 + queue-microtask: 1.2.3 + randombytes: 2.1.0 + readable-stream: 3.6.2 + transitivePeerDependencies: + - supports-color + simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 @@ -14242,6 +15420,10 @@ snapshots: dependencies: whatwg-url: 7.1.0 + sparse-bitfield@3.0.3: + dependencies: + memory-pager: 1.5.0 + spawndamnit@3.0.1: dependencies: cross-spawn: 7.0.6 @@ -14636,6 +15818,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tweetnacl@1.0.3: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -14784,6 +15968,8 @@ snapshots: dependencies: normalize-path: 2.1.1 + unload@2.4.1: {} + unpipe@1.0.0: {} unplugin-utils@0.2.4: @@ -14901,6 +16087,14 @@ snapshots: util-deprecate@1.0.2: {} + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.0 + is-typed-array: 1.1.15 + which-typed-array: 1.1.19 + utils-merge@1.0.1: {} uuid@11.1.0: {} @@ -14912,6 +16106,8 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + validator@13.15.15: {} + vary@1.1.2: {} vite-node@3.2.4(@types/node@22.17.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): @@ -15078,6 +16274,8 @@ snapshots: web-streams-polyfill@3.3.3: {} + web-vitals@4.2.4: {} + web-vitals@5.0.3: {} webidl-conversions@3.0.1: {} @@ -15088,6 +16286,14 @@ snapshots: webpack-virtual-modules@0.6.2: {} + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.10 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 @@ -15279,6 +16485,14 @@ snapshots: cookie: 1.0.2 youch-core: 0.3.3 + z-schema@6.0.2: + dependencies: + lodash.get: 4.4.2 + lodash.isequal: 4.5.0 + validator: 13.15.15 + optionalDependencies: + commander: 11.1.0 + zimmerframe@1.1.2: {} zip-stream@6.0.1: From 195a5829f3cea5bad8f7b43b0ab3bfc9d71cc91d Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:12:58 +0200 Subject: [PATCH 11/18] ADD test for multi-tab --- .../rxdb-db-collection/tests/rxdb.test.ts | 66 ++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/packages/rxdb-db-collection/tests/rxdb.test.ts b/packages/rxdb-db-collection/tests/rxdb.test.ts index 40ee231f3..35567be5f 100644 --- a/packages/rxdb-db-collection/tests/rxdb.test.ts +++ b/packages/rxdb-db-collection/tests/rxdb.test.ts @@ -56,12 +56,15 @@ describe(`RxDB Integration`, () => { } }) } - async function createTestState( + + + async function getDatababase( initialDocs: TestDocType[] = [], - config: Partial> = {} + dbId = dbNameId++ ) { const db = await createRxDatabase({ - name: 'my-rxdb-' + (dbNameId++), + name: 'my-rxdb-' + dbId, + ignoreDuplicate: true, storage: wrappedValidateAjvStorage({ storage: getRxStorageMemory() }) @@ -90,7 +93,18 @@ describe(`RxDB Integration`, () => { const insertResult = await rxCollection.bulkInsert(initialDocs) expect(insertResult.error.length).toBe(0) } + return db + } + async function createTestState( + initialDocs: TestDocType[] = [], + config: Partial> = {} + ) { + const db = await getDatababase( + initialDocs, + dbNameId++ + ) + const rxCollection: RxCollection = db.test; const options = rxdbCollectionOptions({ rxCollection: rxCollection, startSync: true, @@ -252,6 +266,52 @@ describe(`RxDB Integration`, () => { }) }) + /** + * Here we simulate having multiple browser tabs + * open where the data should still be in sync. + */ + describe(`multi tab`, () => { + it(`should update the state accross instances`, async () => { + const dbid = dbNameId++ + const db1 = await getDatababase([], dbid) + const db2 = await getDatababase(getTestData(2), dbid) + + const col1 = createCollection( + rxdbCollectionOptions({ + rxCollection: db1.test, + startSync: true, + }) + ) + const col2 = createCollection( + rxdbCollectionOptions({ + rxCollection: db2.test, + startSync: true, + }) + ) + await col1.stateWhenReady() + await col2.stateWhenReady() + + // tanstack-db writes + col1.insert({ id: 't1', name: 't1' }) + col2.insert({ id: 't2', name: 't2' }) + await db1.test.findOne('t1').exec(true) + await db1.test.findOne('t2').exec(true) + await db2.test.findOne('t1').exec(true) + await db2.test.findOne('t2').exec(true) + expect(col2.get(`t1`)).toBeTruthy() + expect(col1.get(`t2`)).toBeTruthy() + + // RxDB writes + await db1.test.insert({ id: 'rx1', name: 'rx1' }) + await db2.test.insert({ id: 'rx2', name: 'rx2' }) + expect(col2.get(`rx1`)).toBeTruthy() + expect(col1.get(`rx2`)).toBeTruthy() + + db1.remove() + db2.remove() + }) + }) + describe('error handling', () => { it('should rollback the transaction on invalid data that does not match the RxCollection schema', async () => { const initialItems = getTestData(2) From 8072a6847d0d05ea15a9d3c235080c47c1e3a407 Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:22:50 +0200 Subject: [PATCH 12/18] FIX lint --- packages/rxdb-db-collection/src/helper.ts | 26 +- packages/rxdb-db-collection/src/rxdb.ts | 478 +++++++-------- .../rxdb-db-collection/tests/rxdb.test.ts | 571 ++++++++---------- 3 files changed, 516 insertions(+), 559 deletions(-) diff --git a/packages/rxdb-db-collection/src/helper.ts b/packages/rxdb-db-collection/src/helper.ts index 57678bb37..096005edd 100644 --- a/packages/rxdb-db-collection/src/helper.ts +++ b/packages/rxdb-db-collection/src/helper.ts @@ -1,16 +1,18 @@ const RESERVED_RXDB_FIELDS = new Set([ - '_rev', - '_deleted', - '_attachments', - '_meta', + `_rev`, + `_deleted`, + `_attachments`, + `_meta`, ]) -export function stripRxdbFields>(obj: T): T { - if (!obj) return obj - const out: any = Array.isArray(obj) ? [] : {} - for (const k of Object.keys(obj)) { - if (RESERVED_RXDB_FIELDS.has(k)) continue - out[k] = obj[k] - } - return out as T +export function stripRxdbFields>( + obj: T | any +): T { + if (!obj) return obj + const out: any = Array.isArray(obj) ? [] : {} + for (const k of Object.keys(obj)) { + if (RESERVED_RXDB_FIELDS.has(k)) continue + out[k] = obj[k] + } + return out as T } diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index 0f1ee64de..fdc6d0052 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -1,33 +1,32 @@ import { - FilledMangoQuery, - RxCollection, - RxDocumentData, - clone, - ensureNotFalsy, - getFromMapOrCreate, - lastOfArray, - prepareQuery, - rxStorageWriteErrorToRxError + clone, + ensureNotFalsy, + getFromMapOrCreate, + lastOfArray, + prepareQuery, + rxStorageWriteErrorToRxError, } from "rxdb/plugins/core" -import type { Subscription } from 'rxjs' - import DebugModule from "debug" +import { stripRxdbFields } from "./helper" import type { - CollectionConfig, - ResolveType, - SyncConfig, -} from "@tanstack/db" + FilledMangoQuery, + RxCollection, + RxDocumentData, +} from "rxdb/plugins/core" +import type { Subscription } from "rxjs" + +import type { CollectionConfig, ResolveType, SyncConfig } from "@tanstack/db" import type { StandardSchemaV1 } from "@standard-schema/spec" -import { stripRxdbFields } from './helper' const debug = DebugModule.debug(`ts/db:rxdb`) - /** * Used in tests to ensure proper cleanup */ -export const OPEN_RXDB_SUBSCRIPTIONS = new WeakMap>() - +export const OPEN_RXDB_SUBSCRIPTIONS = new WeakMap< + RxCollection, + Set +>() /** * Configuration interface for RxDB collection options @@ -43,37 +42,33 @@ export const OPEN_RXDB_SUBSCRIPTIONS = new WeakMap, - TSchema extends StandardSchemaV1 = never + TExplicit extends object = Record, + TSchema extends StandardSchemaV1 = never, > = Omit< - CollectionConfig< - ResolveType, - string, - TSchema - >, - 'insert' | 'update' | 'delete' | 'getKey' | 'sync' + CollectionConfig, string, TSchema>, + `insert` | `update` | `delete` | `getKey` | `sync` > & { - /** - * The RxCollection from a RxDB Database instance. - */ - rxCollection: RxCollection + /** + * The RxCollection from a RxDB Database instance. + */ + rxCollection: RxCollection - /** - * The maximum number of documents to read from the RxDB collection - * in a single batch during the initial sync between RxDB and the - * in-memory TanStack DB collection. - * - * @remarks - * - Defaults to `1000` if not specified. - * - Larger values reduce the number of round trips to the storage - * engine but increase memory usage per batch. - * - Smaller values may lower memory usage and allow earlier - * streaming of initial results, at the cost of more query calls. - * - * Adjust this depending on your expected collection size and - * performance characteristics of the chosen RxDB storage adapter. - */ - syncBatchSize?: number + /** + * The maximum number of documents to read from the RxDB collection + * in a single batch during the initial sync between RxDB and the + * in-memory TanStack DB collection. + * + * @remarks + * - Defaults to `1000` if not specified. + * - Larger values reduce the number of round trips to the storage + * engine but increase memory usage per batch. + * - Smaller values may lower memory usage and allow earlier + * streaming of initial results, at the cost of more query calls. + * + * Adjust this depending on your expected collection size and + * performance characteristics of the chosen RxDB storage adapter. + */ + syncBatchSize?: number } /** @@ -85,219 +80,216 @@ export type RxDBCollectionConfig< * @returns Collection options with utilities */ export function rxdbCollectionOptions< - TExplicit extends object = Record, - TSchema extends StandardSchemaV1 = never ->( - config: RxDBCollectionConfig -) { - type Row = ResolveType; - type Key = string; // because RxDB primary keys must be strings - - const { ...restConfig } = config - const rxCollection = config.rxCollection + TExplicit extends object = Record, + TSchema extends StandardSchemaV1 = never, +>(config: RxDBCollectionConfig) { + type Row = ResolveType + type Key = string // because RxDB primary keys must be strings - // "getKey" - const primaryPath = rxCollection.schema.primaryPath - const getKey: CollectionConfig['getKey'] = (item) => { - const key: string = (item as any)[primaryPath] as string - return key - } + const { ...restConfig } = config + const rxCollection = config.rxCollection - /** - * "sync" - * Notice that this describes the Sync between the local RxDB collection - * and the in-memory tanstack-db collection. - * It is not about sync between a client and a server! - */ - type SyncParams = Parameters['sync']>[0] - const sync: SyncConfig = { - sync: (params: SyncParams) => { - const { begin, write, commit, markReady } = params + // "getKey" + const primaryPath = rxCollection.schema.primaryPath + const getKey: CollectionConfig[`getKey`] = (item) => { + const key: string = (item as any)[primaryPath] as string + return key + } - let ready = false - async function initialFetch() { - /** - * RxDB stores a last-write-time - * which can be used to "sort" document writes, - * so for initial sync we iterate over that. - */ - let cursor: RxDocumentData | undefined = undefined - const syncBatchSize = config.syncBatchSize ? config.syncBatchSize : 1000 - begin() + /** + * "sync" + * Notice that this describes the Sync between the local RxDB collection + * and the in-memory tanstack-db collection. + * It is not about sync between a client and a server! + */ + type SyncParams = Parameters[`sync`]>[0] + const sync: SyncConfig = { + sync: (params: SyncParams) => { + const { begin, write, commit, markReady } = params - while (!ready) { - let query: FilledMangoQuery - if (cursor) { - query = { - selector: { - $or: [ - { '_meta.lwt': { $gt: (cursor._meta.lwt as number) } }, - { - '_meta.lwt': cursor._meta.lwt, - [primaryPath]: { - $gt: cursor[primaryPath] - }, - } - ] - } as any, - sort: [ - { '_meta.lwt': 'asc' }, - { [primaryPath]: 'asc' } as any - ], - limit: syncBatchSize, - skip: 0 - } - } else { - query = { - selector: {}, - sort: [ - { '_meta.lwt': 'asc' }, - { [primaryPath]: 'asc' } as any - ], - limit: syncBatchSize, - skip: 0 - } - } + let ready = false + async function initialFetch() { + /** + * RxDB stores a last-write-time + * which can be used to "sort" document writes, + * so for initial sync we iterate over that. + */ + let cursor: RxDocumentData | undefined = undefined + const syncBatchSize = config.syncBatchSize ? config.syncBatchSize : 1000 + begin() - /** - * Instead of doing a RxCollection.query(), - * we directly query the storage engine of the RxCollection so we do not use the - * RxCollection document cache because it likely wont be used anyway - * since most queries will run directly on the tanstack-db side. - */ - const preparedQuery = prepareQuery( - rxCollection.storageInstance.schema, - query - ); - const result = await rxCollection.storageInstance.query(preparedQuery) - const docs = result.documents - - cursor = lastOfArray(docs) - if (docs.length === 0) { - ready = true - break; - } - - docs.forEach(d => { - write({ - type: 'insert', - value: stripRxdbFields(clone(d)) as any - }) - }) - - } - commit() + while (!ready) { + let query: FilledMangoQuery + if (cursor) { + query = { + selector: { + $or: [ + { "_meta.lwt": { $gt: cursor._meta.lwt } }, + { + "_meta.lwt": cursor._meta.lwt, + [primaryPath]: { + $gt: cursor[primaryPath], + }, + }, + ], + } as any, + sort: [{ "_meta.lwt": `asc` }, { [primaryPath]: `asc` } as any], + limit: syncBatchSize, + skip: 0, } - - type WriteMessage = Parameters[0] - const buffer: WriteMessage[] = [] - const queue = (msg: WriteMessage) => { - if (!ready) { - buffer.push(msg) - return - } - begin() - write(msg as any) - commit() + } else { + query = { + selector: {}, + sort: [{ "_meta.lwt": `asc` }, { [primaryPath]: `asc` } as any], + limit: syncBatchSize, + skip: 0, } + } - let sub: Subscription - async function startOngoingFetch() { - // Subscribe early and buffer live changes during initial load and ongoing - sub = rxCollection.$.subscribe((ev) => { - const cur = stripRxdbFields(clone(ev.documentData as Row)) - switch (ev.operation) { - case 'INSERT': - if (cur) queue({ type: 'insert', value: cur }) - break - case 'UPDATE': - if (cur) queue({ type: 'update', value: cur }) - break - case 'DELETE': - queue({ type: 'delete', value: cur }) - break - } - }) + /** + * Instead of doing a RxCollection.query(), + * we directly query the storage engine of the RxCollection so we do not use the + * RxCollection document cache because it likely wont be used anyway + * since most queries will run directly on the tanstack-db side. + */ + const preparedQuery = prepareQuery( + rxCollection.storageInstance.schema, + query + ) + const result = await rxCollection.storageInstance.query(preparedQuery) + const docs = result.documents - const subs = getFromMapOrCreate( - OPEN_RXDB_SUBSCRIPTIONS, - rxCollection, - () => new Set() - ) - subs.add(sub) - } + cursor = lastOfArray(docs) + if (docs.length === 0) { + ready = true + break + } + + docs.forEach((d) => { + write({ + type: `insert`, + value: stripRxdbFields(clone(d)), + }) + }) + } + commit() + } + type WriteMessage = Parameters[0] + const buffer: Array = [] + const queue = (msg: WriteMessage) => { + if (!ready) { + buffer.push(msg) + return + } + begin() + write(msg as any) + commit() + } - async function start() { - startOngoingFetch() - await initialFetch(); + let sub: Subscription + function startOngoingFetch() { + // Subscribe early and buffer live changes during initial load and ongoing + sub = rxCollection.$.subscribe((ev) => { + const cur = stripRxdbFields(clone(ev.documentData as Row)) + switch (ev.operation) { + case `INSERT`: + queue({ type: `insert`, value: cur }) + break + case `UPDATE`: + queue({ type: `update`, value: cur }) + break + case `DELETE`: + queue({ type: `delete`, value: cur }) + break + } + }) - if (buffer.length) { - begin() - for (const msg of buffer) write(msg as any) - commit() - buffer.length = 0 - } + const subs = getFromMapOrCreate( + OPEN_RXDB_SUBSCRIPTIONS, + rxCollection, + () => new Set() + ) + subs.add(sub) + } - markReady() - } + async function start() { + startOngoingFetch() + await initialFetch() + if (buffer.length) { + begin() + for (const msg of buffer) write(msg as any) + commit() + buffer.length = 0 + } - start() + markReady() + } - return () => { - const subs = getFromMapOrCreate( - OPEN_RXDB_SUBSCRIPTIONS, - rxCollection, - () => new Set() - ) - subs.delete(sub) - sub.unsubscribe() - } - }, - // Expose the getSyncMetadata function - getSyncMetadata: undefined, - } + start() - const collectionConfig: CollectionConfig> = { - ...restConfig, - getKey, - sync, - onInsert: async (params) => { - debug("insert", params) - const newItems = params.transaction.mutations.map(m => m.modified) - return rxCollection.bulkUpsert(newItems as any).then(result => { - if (result.error.length > 0) { - throw rxStorageWriteErrorToRxError(ensureNotFalsy(result.error[0])) - } - return result.success - }) - }, - onUpdate: async (params) => { - debug("update", params) - const mutations = params.transaction.mutations.filter(m => m.type === 'update') + return () => { + const subs = getFromMapOrCreate( + OPEN_RXDB_SUBSCRIPTIONS, + rxCollection, + () => new Set() + ) + subs.delete(sub) + sub.unsubscribe() + } + }, + // Expose the getSyncMetadata function + getSyncMetadata: undefined, + } - for (const mutation of mutations) { - const newValue = stripRxdbFields(mutation.modified) - const id = (newValue as any)[primaryPath] - const doc = await rxCollection.findOne(id).exec() - if (!doc) { - continue - } - await doc.incrementalPatch(newValue as any) - } - }, - onDelete: async (params) => { - debug("delete", params) - const mutations = params.transaction.mutations.filter(m => m.type === 'delete') - const ids = mutations.map(mutation => (mutation.original as any)[primaryPath]) - return rxCollection.bulkRemove(ids).then(result => { - if (result.error.length > 0) { - throw result.error - } - return result.success - }) + const collectionConfig: CollectionConfig< + ResolveType + > = { + ...restConfig, + getKey, + sync, + onInsert: async (params) => { + debug(`insert`, params) + const newItems = params.transaction.mutations.map((m) => m.modified) + return rxCollection.bulkUpsert(newItems as any).then((result) => { + if (result.error.length > 0) { + throw rxStorageWriteErrorToRxError(ensureNotFalsy(result.error[0])) + } + return result.success + }) + }, + onUpdate: async (params) => { + debug(`update`, params) + const mutations = params.transaction.mutations.filter( + (m) => m.type === `update` + ) + + for (const mutation of mutations) { + const newValue = stripRxdbFields(mutation.modified) + const id = (newValue as any)[primaryPath] + const doc = await rxCollection.findOne(id).exec() + if (!doc) { + continue + } + await doc.incrementalPatch(newValue as any) + } + }, + onDelete: async (params) => { + debug(`delete`, params) + const mutations = params.transaction.mutations.filter( + (m) => m.type === `delete` + ) + const ids = mutations.map( + (mutation) => (mutation.original as any)[primaryPath] + ) + return rxCollection.bulkRemove(ids).then((result) => { + if (result.error.length > 0) { + throw result.error } - } - return collectionConfig; + return result.success + }) + }, + } + return collectionConfig } diff --git a/packages/rxdb-db-collection/tests/rxdb.test.ts b/packages/rxdb-db-collection/tests/rxdb.test.ts index 35567be5f..d06e52cbb 100644 --- a/packages/rxdb-db-collection/tests/rxdb.test.ts +++ b/packages/rxdb-db-collection/tests/rxdb.test.ts @@ -1,350 +1,313 @@ -import { describe, expect, it, vi } from "vitest" +import { describe, expect, it } from "vitest" +import { createCollection } from "@tanstack/db" import { - createCollection, -} from "@tanstack/db" -import { - OPEN_RXDB_SUBSCRIPTIONS, - RxDBCollectionConfig, - rxdbCollectionOptions -} from "../src/rxdb" -import type { - Collection, - UtilsRecord, -} from "@tanstack/db" -import type { StandardSchemaV1 } from "@standard-schema/spec" -import { - RxCollection, - addRxPlugin, - createRxDatabase, - getFromMapOrCreate -} from 'rxdb/plugins/core' -import { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode' -import { getRxStorageMemory } from 'rxdb/plugins/storage-memory' -import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv' - -// Mock the ShapeStream module -const mockSubscribe = vi.fn() -const mockStream = { - subscribe: mockSubscribe, -} + addRxPlugin, + createRxDatabase, + getFromMapOrCreate, +} from "rxdb/plugins/core" +import { RxDBDevModePlugin } from "rxdb/plugins/dev-mode" +import { getRxStorageMemory } from "rxdb/plugins/storage-memory" +import { wrappedValidateAjvStorage } from "rxdb/plugins/validate-ajv" +import { OPEN_RXDB_SUBSCRIPTIONS, rxdbCollectionOptions } from "../src/rxdb" +import type { RxCollection } from "rxdb/plugins/core" +import type { RxDBCollectionConfig } from "../src/rxdb" type TestDocType = { - id: string - name: string + id: string + name: string } -type RxCollections = { test: RxCollection }; +type RxCollections = { test: RxCollection } // Helper to advance timers and allow microtasks to flush const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0)) describe(`RxDB Integration`, () => { - addRxPlugin(RxDBDevModePlugin) - let collection: Collection< - any, - string | number, - UtilsRecord, - StandardSchemaV1, - any - >; - - let dbNameId = 0; - function getTestData(amount: number): Array { - return new Array(amount).fill(0).map((_v, i) => { - return { - id: (i + 1) + '', - name: 'Item ' + (i + 1) - } - }) - } - - - async function getDatababase( - initialDocs: TestDocType[] = [], - dbId = dbNameId++ - ) { - const db = await createRxDatabase({ - name: 'my-rxdb-' + dbId, - ignoreDuplicate: true, - storage: wrappedValidateAjvStorage({ - storage: getRxStorageMemory() - }) - }); - const collections = await db.addCollections({ - test: { - schema: { - version: 0, - type: 'object', - primaryKey: 'id', - properties: { - id: { - type: 'string', - maxLength: 100 - }, - name: { - type: 'string', - maxLength: 9 - } - } - } - } - }); - const rxCollection: RxCollection = collections.test; - if (initialDocs.length > 0) { - const insertResult = await rxCollection.bulkInsert(initialDocs) - expect(insertResult.error.length).toBe(0) - } - return db + addRxPlugin(RxDBDevModePlugin) + + let dbNameId = 0 + function getTestData(amount: number): Array { + return new Array(amount).fill(0).map((_v, i) => { + return { + id: i + 1 + ``, + name: `Item ` + (i + 1), + } + }) + } + + async function getDatababase( + initialDocs: Array = [], + dbId = dbNameId++ + ) { + const db = await createRxDatabase( + { + name: `my-rxdb-` + dbId, + ignoreDuplicate: true, + storage: wrappedValidateAjvStorage({ + storage: getRxStorageMemory(), + }), + } + ) + const collections = await db.addCollections({ + test: { + schema: { + version: 0, + type: `object`, + primaryKey: `id`, + properties: { + id: { + type: `string`, + maxLength: 100, + }, + name: { + type: `string`, + maxLength: 9, + }, + }, + }, + }, + }) + const rxCollection: RxCollection = collections.test + if (initialDocs.length > 0) { + const insertResult = await rxCollection.bulkInsert(initialDocs) + expect(insertResult.error.length).toBe(0) } + return db + } + + async function createTestState( + initialDocs: Array = [], + config: Partial> = {} + ) { + const db = await getDatababase(initialDocs, dbNameId++) + const rxCollection: RxCollection = db.test + const options = rxdbCollectionOptions({ + rxCollection: rxCollection, + startSync: true, + /** + * In tests we use a small batch size + * to ensure iteration works. + */ + syncBatchSize: 10, + ...config, + }) - async function createTestState( - initialDocs: TestDocType[] = [], - config: Partial> = {} - ) { - const db = await getDatababase( - initialDocs, - dbNameId++ - ) - const rxCollection: RxCollection = db.test; - const options = rxdbCollectionOptions({ - rxCollection: rxCollection, - startSync: true, - /** - * In tests we use a small batch size - * to ensure iteration works. - */ - syncBatchSize: 10, - ...config - }) - - collection = createCollection(options) - await collection.stateWhenReady() + const collection = createCollection(options) + await collection.stateWhenReady() - return { - collection, - rxCollection, - db - } + return { + collection, + rxCollection, + db, } + } - describe('sync', () => { - it(`should initialize and fetch initial data`, async () => { - const initialItems = getTestData(2) - - const { collection, db } = await createTestState(initialItems); - - // Verify the collection state contains our items - expect(collection.size).toBe(initialItems.length) - expect(collection.get(`1`)).toEqual(initialItems[0]) - expect(collection.get(`2`)).toEqual(initialItems[1]) - - // Verify the synced data - expect(collection.syncedData.size).toBe(initialItems.length) - expect(collection.syncedData.get(`1`)).toEqual(initialItems[0]) - expect(collection.syncedData.get(`2`)).toEqual(initialItems[1]) + describe(`sync`, () => { + it(`should initialize and fetch initial data`, async () => { + const initialItems = getTestData(2) - await db.remove() - }) + const { collection, db } = await createTestState(initialItems) - it('should initialize and fetch initial data with many documents', async () => { - const docsAmount = 25; // > 10 to force multiple batches - const initialItems = getTestData(docsAmount); - const { collection, db } = await createTestState(initialItems); + // Verify the collection state contains our items + expect(collection.size).toBe(initialItems.length) + expect(collection.get(`1`)).toEqual(initialItems[0]) + expect(collection.get(`2`)).toEqual(initialItems[1]) - // All docs should be present after initial sync - expect(collection.size).toBe(docsAmount); - expect(collection.syncedData.size).toBe(docsAmount); + // Verify the synced data + expect(collection.syncedData.size).toBe(initialItems.length) + expect(collection.syncedData.get(`1`)).toEqual(initialItems[0]) + expect(collection.syncedData.get(`2`)).toEqual(initialItems[1]) - // Spot-check a few positions - expect(collection.get('1')).toEqual({ id: '1', name: 'Item 1' }); - expect(collection.get('10')).toEqual({ id: '10', name: 'Item 10' }); - expect(collection.get('11')).toEqual({ id: '11', name: 'Item 11' }); - expect(collection.get('25')).toEqual({ id: '25', name: 'Item 25' }); + await db.remove() + }) - // Ensure no gaps - for (let i = 1; i <= docsAmount; i++) { - expect(collection.has(String(i))).toBe(true); - } + it(`should initialize and fetch initial data with many documents`, async () => { + const docsAmount = 25 // > 10 to force multiple batches + const initialItems = getTestData(docsAmount) + const { collection, db } = await createTestState(initialItems) - await db.remove() - }) + // All docs should be present after initial sync + expect(collection.size).toBe(docsAmount) + expect(collection.syncedData.size).toBe(docsAmount) - it(`should update the collection when RxDB changes data`, async () => { - const initialItems = getTestData(2) + // Spot-check a few positions + expect(collection.get(`1`)).toEqual({ id: `1`, name: `Item 1` }) + expect(collection.get(`10`)).toEqual({ id: `10`, name: `Item 10` }) + expect(collection.get(`11`)).toEqual({ id: `11`, name: `Item 11` }) + expect(collection.get(`25`)).toEqual({ id: `25`, name: `Item 25` }) - const { collection, rxCollection, db } = await createTestState(initialItems); + // Ensure no gaps + for (let i = 1; i <= docsAmount; i++) { + expect(collection.has(String(i))).toBe(true) + } + await db.remove() + }) - // inserts - const doc = await rxCollection.insert({ id: '3', name: 'inserted' }) - expect(collection.get(`3`).name).toEqual('inserted') + it(`should update the collection when RxDB changes data`, async () => { + const initialItems = getTestData(2) - // updates - await doc.getLatest().patch({ name: 'updated' }) - expect(collection.get(`3`).name).toEqual('updated') + const { collection, rxCollection, db } = + await createTestState(initialItems) + // inserts + const doc = await rxCollection.insert({ id: `3`, name: `inserted` }) + expect(collection.get(`3`).name).toEqual(`inserted`) - // deletes - await doc.getLatest().remove() - expect(collection.get(`3`)).toEqual(undefined) + // updates + await doc.getLatest().patch({ name: `updated` }) + expect(collection.get(`3`).name).toEqual(`updated`) - await db.remove() - }) + // deletes + await doc.getLatest().remove() + expect(collection.get(`3`)).toEqual(undefined) - it(`should update RxDB when the collection changes data`, async () => { - const initialItems = getTestData(2) - - const { collection, rxCollection, db } = await createTestState(initialItems); + await db.remove() + }) + it(`should update RxDB when the collection changes data`, async () => { + const initialItems = getTestData(2) + + const { collection, rxCollection, db } = + await createTestState(initialItems) + + // inserts + const tx = collection.insert({ id: `3`, name: `inserted` }) + await tx.isPersisted.promise + let doc = await rxCollection.findOne(`3`).exec(true) + expect(doc.name).toEqual(`inserted`) + + // updates + collection.update(`3`, (d) => { + d.name = `updated` + }) + expect(collection.get(`3`).name).toEqual(`updated`) + await collection.stateWhenReady() + await rxCollection.database.requestIdlePromise() + doc = await rxCollection.findOne(`3`).exec(true) + expect(doc.name).toEqual(`updated`) + + // deletes + collection.delete(`3`) + await rxCollection.database.requestIdlePromise() + const mustNotBeFound = await rxCollection.findOne(`3`).exec() + expect(mustNotBeFound).toEqual(null) + + await db.remove() + }) + }) - // inserts - const tx = collection.insert({ id: `3`, name: `inserted` }) - await tx.isPersisted.promise - let doc = await rxCollection.findOne('3').exec(true) - expect(doc.name).toEqual('inserted') + describe(`lifecycle management`, () => { + it(`should call unsubscribe when collection is cleaned up`, async () => { + const { collection, rxCollection, db } = await createTestState() - // updates - collection.update( - '3', - d => { - d.name = 'updated' - } - ) - expect(collection.get(`3`).name).toEqual('updated') - await collection.stateWhenReady() - await rxCollection.database.requestIdlePromise() - doc = await rxCollection.findOne('3').exec(true) - expect(doc.name).toEqual('updated') + await collection.cleanup() + const subs = getFromMapOrCreate( + OPEN_RXDB_SUBSCRIPTIONS, + rxCollection, + () => new Set() + ) + expect(subs.size).toEqual(0) - // deletes - collection.delete('3') - await rxCollection.database.requestIdlePromise() - const mustNotBeFound = await rxCollection.findOne('3').exec() - expect(mustNotBeFound).toEqual(null) + await db.remove() + }) - await db.remove() - }) - }); + it(`should restart sync when collection is accessed after cleanup`, async () => { + const initialItems = getTestData(2) + const { collection, rxCollection, db } = + await createTestState(initialItems) - describe(`lifecycle management`, () => { - it(`should call unsubscribe when collection is cleaned up`, async () => { - const { collection, rxCollection, db } = await createTestState(); + await collection.cleanup() + await flushPromises() + expect(collection.status).toBe(`cleaned-up`) - await collection.cleanup() + // insert into RxDB while cleaned-up + await rxCollection.insert({ id: `3`, name: `Item 3` }) - const subs = getFromMapOrCreate( - OPEN_RXDB_SUBSCRIPTIONS, - rxCollection, - () => new Set() - ) - expect(subs.size).toEqual(0) + // Access collection data to restart sync + const unsubscribe = collection.subscribeChanges(() => {}) + await collection.toArrayWhenReady() + expect(collection.get(`3`).name).toEqual(`Item 3`) - await db.remove() + unsubscribe() + await db.remove() + }) + }) + + /** + * Here we simulate having multiple browser tabs + * open where the data should still be in sync. + */ + describe(`multi tab`, () => { + it(`should update the state accross instances`, async () => { + const dbid = dbNameId++ + const db1 = await getDatababase([], dbid) + const db2 = await getDatababase(getTestData(2), dbid) + + const col1 = createCollection( + rxdbCollectionOptions({ + rxCollection: db1.test, + startSync: true, }) - - it(`should restart sync when collection is accessed after cleanup`, async () => { - const initialItems = getTestData(2) - const { collection, rxCollection, db } = await createTestState(initialItems); - - await collection.cleanup() - await flushPromises() - expect(collection.status).toBe(`cleaned-up`) - - // insert into RxDB while cleaned-up - await rxCollection.insert({ id: '3', name: 'Item 3' }) - - // Access collection data to restart sync - const unsubscribe = collection.subscribeChanges(() => { }) - - await collection.toArrayWhenReady() - expect(collection.get(`3`).name).toEqual('Item 3') - - - unsubscribe() - await db.remove() + ) + const col2 = createCollection( + rxdbCollectionOptions({ + rxCollection: db2.test, + startSync: true, }) + ) + await col1.stateWhenReady() + await col2.stateWhenReady() + + // tanstack-db writes + col1.insert({ id: `t1`, name: `t1` }) + col2.insert({ id: `t2`, name: `t2` }) + await db1.test.findOne(`t1`).exec(true) + await db1.test.findOne(`t2`).exec(true) + await db2.test.findOne(`t1`).exec(true) + await db2.test.findOne(`t2`).exec(true) + expect(col2.get(`t1`)).toBeTruthy() + expect(col1.get(`t2`)).toBeTruthy() + + // RxDB writes + await db1.test.insert({ id: `rx1`, name: `rx1` }) + await db2.test.insert({ id: `rx2`, name: `rx2` }) + expect(col2.get(`rx1`)).toBeTruthy() + expect(col1.get(`rx2`)).toBeTruthy() + + db1.remove() + db2.remove() }) - - /** - * Here we simulate having multiple browser tabs - * open where the data should still be in sync. - */ - describe(`multi tab`, () => { - it(`should update the state accross instances`, async () => { - const dbid = dbNameId++ - const db1 = await getDatababase([], dbid) - const db2 = await getDatababase(getTestData(2), dbid) - - const col1 = createCollection( - rxdbCollectionOptions({ - rxCollection: db1.test, - startSync: true, - }) - ) - const col2 = createCollection( - rxdbCollectionOptions({ - rxCollection: db2.test, - startSync: true, - }) - ) - await col1.stateWhenReady() - await col2.stateWhenReady() - - // tanstack-db writes - col1.insert({ id: 't1', name: 't1' }) - col2.insert({ id: 't2', name: 't2' }) - await db1.test.findOne('t1').exec(true) - await db1.test.findOne('t2').exec(true) - await db2.test.findOne('t1').exec(true) - await db2.test.findOne('t2').exec(true) - expect(col2.get(`t1`)).toBeTruthy() - expect(col1.get(`t2`)).toBeTruthy() - - // RxDB writes - await db1.test.insert({ id: 'rx1', name: 'rx1' }) - await db2.test.insert({ id: 'rx2', name: 'rx2' }) - expect(col2.get(`rx1`)).toBeTruthy() - expect(col1.get(`rx2`)).toBeTruthy() - - db1.remove() - db2.remove() + }) + + describe(`error handling`, () => { + it(`should rollback the transaction on invalid data that does not match the RxCollection schema`, async () => { + const initialItems = getTestData(2) + const { collection, db } = await createTestState(initialItems) + + // INSERT + await expect(async () => { + const tx = await collection.insert({ + id: `3`, + name: `invalid`, + foo: `bar`, }) - }) - - describe('error handling', () => { - it('should rollback the transaction on invalid data that does not match the RxCollection schema', async () => { - const initialItems = getTestData(2) - const { collection, db } = await createTestState(initialItems); - - // INSERT - await expect(async () => { - const tx = await collection.insert({ - id: '3', - name: 'invalid', - foo: 'bar' - }) - await tx.isPersisted.promise - }).rejects.toThrow(/schema validation error/) - expect(collection.has('3')).toBe(false) - - // UPDATE - await expect(async () => { - const tx = await collection.update( - '2', - d => { - d.name = 'invalid' - d.foo = 'bar' - } - ) - await tx.isPersisted.promise - }).rejects.toThrow(/schema validation error/) - expect(collection.get('2').name).toBe('Item 2') - - - await db.remove() + await tx.isPersisted.promise + }).rejects.toThrow(/schema validation error/) + expect(collection.has(`3`)).toBe(false) + + // UPDATE + await expect(async () => { + const tx = await collection.update(`2`, (d) => { + d.name = `invalid` + d.foo = `bar` }) - }) + await tx.isPersisted.promise + }).rejects.toThrow(/schema validation error/) + expect(collection.get(`2`).name).toBe(`Item 2`) - -}); + await db.remove() + }) + }) +}) From 70cb964074133ac66b5bfb22803c14e56bc49ba5 Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:11:09 +0200 Subject: [PATCH 13/18] FIX lint --- packages/rxdb-db-collection/src/rxdb.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index fdc6d0052..bbc447eb6 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -13,7 +13,6 @@ import type { RxCollection, RxDocumentData, } from "rxdb/plugins/core" -import type { Subscription } from "rxjs" import type { CollectionConfig, ResolveType, SyncConfig } from "@tanstack/db" import type { StandardSchemaV1 } from "@standard-schema/spec" @@ -25,7 +24,7 @@ const debug = DebugModule.debug(`ts/db:rxdb`) */ export const OPEN_RXDB_SUBSCRIPTIONS = new WeakMap< RxCollection, - Set + Set >() /** @@ -187,11 +186,11 @@ export function rxdbCollectionOptions< commit() } - let sub: Subscription + let sub: any function startOngoingFetch() { // Subscribe early and buffer live changes during initial load and ongoing sub = rxCollection.$.subscribe((ev) => { - const cur = stripRxdbFields(clone(ev.documentData as Row)) + const cur: ResolveType = stripRxdbFields(clone(ev.documentData as Row)) switch (ev.operation) { case `INSERT`: queue({ type: `insert`, value: cur }) From 9869cfef55d8026335600ccddc01251b61657384 Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:21:55 +0200 Subject: [PATCH 14/18] ADD allow manual trigger of CI --- .github/workflows/pr.yml | 3 +++ packages/rxdb-db-collection/src/rxdb.ts | 9 ++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 52c20c570..e7240c769 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -3,6 +3,9 @@ name: PR on: pull_request: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + concurrency: group: ${{ github.workflow }}-${{ github.event.number || github.ref }} cancel-in-progress: true diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index bbc447eb6..74ea4d7ad 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -22,10 +22,7 @@ const debug = DebugModule.debug(`ts/db:rxdb`) /** * Used in tests to ensure proper cleanup */ -export const OPEN_RXDB_SUBSCRIPTIONS = new WeakMap< - RxCollection, - Set ->() +export const OPEN_RXDB_SUBSCRIPTIONS = new WeakMap>() /** * Configuration interface for RxDB collection options @@ -190,7 +187,9 @@ export function rxdbCollectionOptions< function startOngoingFetch() { // Subscribe early and buffer live changes during initial load and ongoing sub = rxCollection.$.subscribe((ev) => { - const cur: ResolveType = stripRxdbFields(clone(ev.documentData as Row)) + const cur: ResolveType = stripRxdbFields( + clone(ev.documentData as Row) + ) switch (ev.operation) { case `INSERT`: queue({ type: `insert`, value: cur }) From f077fcf452ac307ccbbd94fb9c44ec74d8fdec41 Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:26:31 +0200 Subject: [PATCH 15/18] FIX Types --- packages/rxdb-db-collection/src/rxdb.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index 74ea4d7ad..8985c6849 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -86,7 +86,7 @@ export function rxdbCollectionOptions< const rxCollection = config.rxCollection // "getKey" - const primaryPath = rxCollection.schema.primaryPath + const primaryPath = rxCollection.schema.primaryPath as string; const getKey: CollectionConfig[`getKey`] = (item) => { const key: string = (item as any)[primaryPath] as string return key @@ -124,7 +124,7 @@ export function rxdbCollectionOptions< { "_meta.lwt": cursor._meta.lwt, [primaryPath]: { - $gt: cursor[primaryPath], + $gt: (cursor as any)[primaryPath], }, }, ], @@ -242,7 +242,7 @@ export function rxdbCollectionOptions< } const collectionConfig: CollectionConfig< - ResolveType + ResolveType > = { ...restConfig, getKey, @@ -250,7 +250,7 @@ export function rxdbCollectionOptions< onInsert: async (params) => { debug(`insert`, params) const newItems = params.transaction.mutations.map((m) => m.modified) - return rxCollection.bulkUpsert(newItems as any).then((result) => { + return rxCollection.bulkUpsert(newItems as any[]).then((result) => { if (result.error.length > 0) { throw rxStorageWriteErrorToRxError(ensureNotFalsy(result.error[0])) } From 7eca4e91c984bf72f5aa8665cfc86f06d0cd2de0 Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:36:09 +0200 Subject: [PATCH 16/18] FIX types --- packages/rxdb-db-collection/src/rxdb.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index 8985c6849..b6d8bb00b 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -87,8 +87,8 @@ export function rxdbCollectionOptions< // "getKey" const primaryPath = rxCollection.schema.primaryPath as string; - const getKey: CollectionConfig[`getKey`] = (item) => { - const key: string = (item as any)[primaryPath] as string + function getKey(item: any): string { + const key: string = item[primaryPath] as string return key } @@ -124,7 +124,7 @@ export function rxdbCollectionOptions< { "_meta.lwt": cursor._meta.lwt, [primaryPath]: { - $gt: (cursor as any)[primaryPath], + $gt: getKey(cursor), }, }, ], @@ -245,7 +245,7 @@ export function rxdbCollectionOptions< ResolveType > = { ...restConfig, - getKey, + getKey: getKey as any, sync, onInsert: async (params) => { debug(`insert`, params) @@ -265,7 +265,7 @@ export function rxdbCollectionOptions< for (const mutation of mutations) { const newValue = stripRxdbFields(mutation.modified) - const id = (newValue as any)[primaryPath] + const id = getKey(newValue); const doc = await rxCollection.findOne(id).exec() if (!doc) { continue @@ -279,7 +279,7 @@ export function rxdbCollectionOptions< (m) => m.type === `delete` ) const ids = mutations.map( - (mutation) => (mutation.original as any)[primaryPath] + (mutation: any) => getKey(mutation.original) ) return rxCollection.bulkRemove(ids).then((result) => { if (result.error.length > 0) { From 64c03d5da89f735c71ce3770dd2dafd755a9fda7 Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Wed, 3 Sep 2025 14:40:50 +0200 Subject: [PATCH 17/18] FIX prettier --- packages/rxdb-db-collection/src/rxdb.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index b6d8bb00b..bbee65ab1 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -86,7 +86,7 @@ export function rxdbCollectionOptions< const rxCollection = config.rxCollection // "getKey" - const primaryPath = rxCollection.schema.primaryPath as string; + const primaryPath = rxCollection.schema.primaryPath as string function getKey(item: any): string { const key: string = item[primaryPath] as string return key @@ -241,9 +241,7 @@ export function rxdbCollectionOptions< getSyncMetadata: undefined, } - const collectionConfig: CollectionConfig< - ResolveType - > = { + const collectionConfig: CollectionConfig> = { ...restConfig, getKey: getKey as any, sync, @@ -265,7 +263,7 @@ export function rxdbCollectionOptions< for (const mutation of mutations) { const newValue = stripRxdbFields(mutation.modified) - const id = getKey(newValue); + const id = getKey(newValue) const doc = await rxCollection.findOne(id).exec() if (!doc) { continue @@ -278,9 +276,7 @@ export function rxdbCollectionOptions< const mutations = params.transaction.mutations.filter( (m) => m.type === `delete` ) - const ids = mutations.map( - (mutation: any) => getKey(mutation.original) - ) + const ids = mutations.map((mutation: any) => getKey(mutation.original)) return rxCollection.bulkRemove(ids).then((result) => { if (result.error.length > 0) { throw result.error From e192105d3ef006f7bda8848e8c8bcf52204028df Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Thu, 11 Sep 2025 10:40:32 -0600 Subject: [PATCH 18/18] doc tweak --- docs/collections/rxdb-collection.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/collections/rxdb-collection.md b/docs/collections/rxdb-collection.md index 09a9470e5..71ea3ffed 100644 --- a/docs/collections/rxdb-collection.md +++ b/docs/collections/rxdb-collection.md @@ -22,8 +22,10 @@ The `@tanstack/rxdb-db-collection` package allows you to create collections that ## 1. Installation +Install the RXDB collection packages along with your preferred framework integration. + ```bash -npm install @tanstack/rxdb-db-collection rxdb @tanstack/db +npm install @tanstack/rxdb-db-collection rxdb @tanstack/react-db ``` @@ -86,7 +88,7 @@ const replicationState = replicateRxCollection({ ### 4. Wrap the RxDB collection with TanStack DB ```ts -import { createCollection } from '@tanstack/db' +import { createCollection } from '@tanstack/react-db' import { rxdbCollectionOptions } from '@tanstack/rxdb-db-collection' const todosCollection = createCollection(