From cbaab3d1a00517e468e42caef657e66c596db553 Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Fri, 25 Apr 2025 12:20:40 -0700 Subject: [PATCH 1/7] Get it working with base64 encoded row ids, and start adding the better way for indexing. --- .../src/module_bindings/index.ts | 5 + packages/sdk/package.json | 3 + packages/sdk/src/algebraic_type.ts | 59 +++- packages/sdk/src/binary_writer.ts | 6 + packages/sdk/src/db_connection_impl.ts | 17 +- packages/sdk/src/spacetime_module.ts | 7 +- packages/sdk/src/table_cache.ts | 27 +- .../test-app/src/module_bindings/index.ts | 12 +- .../src/module_bindings/player_table.ts | 12 +- pnpm-lock.yaml | 281 ++++++++++++++++-- 10 files changed, 385 insertions(+), 44 deletions(-) diff --git a/examples/quickstart-chat/src/module_bindings/index.ts b/examples/quickstart-chat/src/module_bindings/index.ts index 3b7bbfd2..698f69f3 100644 --- a/examples/quickstart-chat/src/module_bindings/index.ts +++ b/examples/quickstart-chat/src/module_bindings/index.ts @@ -63,6 +63,11 @@ const REMOTE_MODULE = { tableName: 'user', rowType: User.getTypeScriptAlgebraicType(), primaryKey: 'identity', + primaryKeyInfo: { + colName: 'identity', + colType: + User.getTypeScriptAlgebraicType().product.elements[0].algebraicType, + }, }, }, reducers: { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index c451a666..54f824e7 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -45,5 +45,8 @@ "@clockworklabs/test-app": "file:../test-app", "tsup": "^8.1.0", "undici": "^6.19.2" + }, + "dependencies": { + "base64-js": "^1.5.1" } } diff --git a/packages/sdk/src/algebraic_type.ts b/packages/sdk/src/algebraic_type.ts index 8f3dcd15..35da90b4 100644 --- a/packages/sdk/src/algebraic_type.ts +++ b/packages/sdk/src/algebraic_type.ts @@ -2,7 +2,7 @@ import { TimeDuration } from './time_duration'; import { Timestamp } from './timestamp'; import { ConnectionId } from './connection_id'; import type BinaryReader from './binary_reader'; -import type BinaryWriter from './binary_writer'; +import BinaryWriter from './binary_writer'; import { Identity } from './identity'; import ScheduleAt from './schedule_at'; @@ -164,6 +164,30 @@ export class ProductType { } }; + intoMapKey(value: any): any { + if (this.elements.length === 1) { + if (this.elements[0].name === '__time_duration_micros__') { + return (value as TimeDuration).__time_duration_micros__; + } + + if (this.elements[0].name === '__timestamp_micros_since_unix_epoch__') { + return (value as Timestamp).__timestamp_micros_since_unix_epoch__; + } + + if (this.elements[0].name === '__identity__') { + return (value as Identity).__identity__; + } + + if (this.elements[0].name === '__connection_id__') { + return (value as ConnectionId).__connection_id__; + } + } + // The fallback is to serialize and base64 encode the bytes. + const writer = new BinaryWriter(10); + this.serialize(writer, value); + return writer.toBase64(); + } + deserialize = (reader: BinaryReader): any => { let result: { [key: string]: any } = {}; if (this.elements.length === 1) { @@ -449,6 +473,39 @@ export class AlgebraicType { return this.#isI64Newtype('__time_duration_micros__'); } + /** + * Convert a value of the algebraic type into something that can be used as a key in a map. + * There are no guarantees about being able to order it. + * This is only guaranteed to be comparable to other values of the same type. + * @param value A value of the algebraic type + * @returns Something that can be used as a key in a map. + */ + intoMapKey(value: any): any { + switch (this.type) { + case Type.U8: + case Type.U16: + case Type.U32: + case Type.U64: + case Type.U128: + case Type.U256: + case Type.I8: + case Type.I16: + case Type.I64: + case Type.I128: + case Type.F32: + case Type.F64: + case Type.String: + case Type.Bool: + return value; + case Type.ProductType: + return this.product.intoMapKey(value); + default: + const writer = new BinaryWriter(10); + this.serialize(writer, value); + return writer.toBase64(); + } + } + serialize(writer: BinaryWriter, value: any): void { switch (this.type) { case Type.ProductType: diff --git a/packages/sdk/src/binary_writer.ts b/packages/sdk/src/binary_writer.ts index b579bfdd..ec43c611 100644 --- a/packages/sdk/src/binary_writer.ts +++ b/packages/sdk/src/binary_writer.ts @@ -1,3 +1,5 @@ +import { fromByteArray } from 'base64-js'; + export default class BinaryWriter { #buffer: Uint8Array; #view: DataView; @@ -19,6 +21,10 @@ export default class BinaryWriter { this.#view = new DataView(this.#buffer.buffer); } + toBase64(): String { + return fromByteArray(this.#buffer.subarray(0, this.#offset)); + } + getBuffer(): Uint8Array { return this.#buffer.slice(0, this.#offset); } diff --git a/packages/sdk/src/db_connection_impl.ts b/packages/sdk/src/db_connection_impl.ts index 6d99d9b1..78d2d83e 100644 --- a/packages/sdk/src/db_connection_impl.ts +++ b/packages/sdk/src/db_connection_impl.ts @@ -54,6 +54,7 @@ import { } from './subscription_builder_impl.ts'; import { stdbLogger } from './logger.ts'; import type { ReducerRuntimeTypeInfo } from './spacetime_module.ts'; +import { fromByteArray } from 'base64-js'; export { AlgebraicType, @@ -306,19 +307,23 @@ export class DbConnectionImpl< ): Operation[] => { const buffer = rowList.rowsData; const reader = new BinaryReader(buffer); - const rows: any[] = []; + const rows: Operation[] = []; const rowType = this.#remoteModule.tables[tableName]!.rowType; while (reader.offset < buffer.length + buffer.byteOffset) { const initialOffset = reader.offset; const row = rowType.deserialize(reader); - // This is super inefficient, but the buffer indexes are weird, so we are doing this for now. - // We should just base64 encode the bytes. - const rowId = JSON.stringify(row, (_, v) => - typeof v === 'bigint' ? v.toString() : v + + // Get a view of the bytes for this row. + const rowBytes = buffer.subarray( + initialOffset - buffer.byteOffset, + reader.offset - buffer.byteOffset ); + // Convert it to a base64 string, so we can use it as a map key. + const asBase64 = fromByteArray(rowBytes); + rows.push({ type, - rowId, + rowId: asBase64, row, }); } diff --git a/packages/sdk/src/spacetime_module.ts b/packages/sdk/src/spacetime_module.ts index 5123c6cb..6ffb61e8 100644 --- a/packages/sdk/src/spacetime_module.ts +++ b/packages/sdk/src/spacetime_module.ts @@ -4,7 +4,12 @@ import type { DbConnectionImpl } from './db_connection_impl'; export interface TableRuntimeTypeInfo { tableName: string; rowType: AlgebraicType; - primaryKey?: string | undefined; + primaryKeyInfo?: PrimaryKeyInfo | undefined; +} + +export interface PrimaryKeyInfo { + colName: string; + colType: AlgebraicType; } export interface ReducerRuntimeTypeInfo { diff --git a/packages/sdk/src/table_cache.ts b/packages/sdk/src/table_cache.ts index d23968c8..ce6139db 100644 --- a/packages/sdk/src/table_cache.ts +++ b/packages/sdk/src/table_cache.ts @@ -2,11 +2,15 @@ import { EventEmitter } from './event_emitter.ts'; import OperationsMap from './operations_map.ts'; import type { TableRuntimeTypeInfo } from './spacetime_module.ts'; -import { type EventContextInterface } from './db_connection_impl.ts'; +import { + BinaryWriter, + type EventContextInterface, +} from './db_connection_impl.ts'; import { stdbLogger } from './logger.ts'; export type Operation = { type: 'insert' | 'delete'; + // rowId: string; rowId: string; row: any; }; @@ -60,17 +64,25 @@ export class TableCache { ctx: EventContextInterface ): PendingCallback[] => { const pendingCallbacks: PendingCallback[] = []; - if (this.tableTypeInfo.primaryKey !== undefined) { - const primaryKey = this.tableTypeInfo.primaryKey; + if (this.tableTypeInfo.primaryKeyInfo !== undefined) { + const primaryKeyCol = this.tableTypeInfo.primaryKeyInfo.colName; + const primaryKeyType = this.tableTypeInfo.primaryKeyInfo.colType; + const getPrimaryKey = (row: any) => { + const primaryKeyValue = row[primaryKeyCol]; + const writer = new BinaryWriter(10); + primaryKeyType.serialize(writer, primaryKeyValue); + return writer.toBase64(); + }; const insertMap = new OperationsMap(); const deleteMap = new OperationsMap(); for (const op of operations) { + const primaryKey = getPrimaryKey(op.row); if (op.type === 'insert') { - const [_, prevCount] = insertMap.get(op.row[primaryKey]) || [op, 0]; - insertMap.set(op.row[primaryKey], [op, prevCount + 1]); + const [_, prevCount] = insertMap.get(primaryKey) || [op, 0]; + insertMap.set(primaryKey, [op, prevCount + 1]); } else { - const [_, prevCount] = deleteMap.get(op.row[primaryKey]) || [op, 0]; - deleteMap.set(op.row[primaryKey], [op, prevCount + 1]); + const [_, prevCount] = deleteMap.get(primaryKey) || [op, 0]; + deleteMap.set(primaryKey, [op, prevCount + 1]); } } for (const { @@ -175,6 +187,7 @@ export class TableCache { }, }; } + console.log(`previousCount of ${previousCount} for ${operation.rowId}`); return undefined; }; diff --git a/packages/test-app/src/module_bindings/index.ts b/packages/test-app/src/module_bindings/index.ts index 2ec61e8e..07395d11 100644 --- a/packages/test-app/src/module_bindings/index.ts +++ b/packages/test-app/src/module_bindings/index.ts @@ -54,12 +54,22 @@ const REMOTE_MODULE = { player: { tableName: 'player', rowType: Player.getTypeScriptAlgebraicType(), - primaryKey: 'owner_id', + primaryKey: 'ownerId', + primaryKeyInfo: { + colName: 'ownerId', + colType: + Player.getTypeScriptAlgebraicType().product.elements[0].algebraicType, + }, }, user: { tableName: 'user', rowType: User.getTypeScriptAlgebraicType(), primaryKey: 'identity', + primaryKeyInfo: { + colName: 'identity', + colType: + User.getTypeScriptAlgebraicType().product.elements[0].algebraicType, + }, }, }, reducers: { diff --git a/packages/test-app/src/module_bindings/player_table.ts b/packages/test-app/src/module_bindings/player_table.ts index a12f00ce..54444468 100644 --- a/packages/test-app/src/module_bindings/player_table.ts +++ b/packages/test-app/src/module_bindings/player_table.ts @@ -60,22 +60,22 @@ export class PlayerTableHandle { return this.tableCache.iter(); } /** - * Access to the `owner_id` unique index on the table `player`, + * Access to the `ownerId` unique index on the table `player`, * which allows point queries on the field of the same name * via the [`PlayerOwnerIdUnique.find`] method. * * Users are encouraged not to explicitly reference this type, * but to directly chain method calls, - * like `ctx.db.player.owner_id().find(...)`. + * like `ctx.db.player.ownerId().find(...)`. * - * Get a handle on the `owner_id` unique index on the table `player`. + * Get a handle on the `ownerId` unique index on the table `player`. */ - owner_id = { - // Find the subscribed row whose `owner_id` column value is equal to `col_val`, + ownerId = { + // Find the subscribed row whose `ownerId` column value is equal to `col_val`, // if such a row is present in the client cache. find: (col_val: string): Player | undefined => { for (let row of this.tableCache.iter()) { - if (deepEqual(row.owner_id, col_val)) { + if (deepEqual(row.ownerId, col_val)) { return row; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0fd8c4bf..fdf97665 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,7 +34,7 @@ importers: version: 5.6.2 vitest: specifier: ^2.0.3 - version: 2.1.2(jsdom@26.0.0)(terser@5.34.1) + version: 2.1.2(@types/node@22.15.0)(jsdom@26.0.0)(terser@5.34.1) examples/quickstart-chat: dependencies: @@ -71,7 +71,7 @@ importers: version: 18.3.5(@types/react@18.3.18) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@6.0.11(terser@5.34.1)(tsx@4.19.1)) + version: 4.3.4(vite@6.0.11(@types/node@22.15.0)(terser@5.34.1)(tsx@4.19.1)) eslint: specifier: ^9.17.0 version: 9.18.0 @@ -95,16 +95,91 @@ importers: version: 8.21.0(eslint@9.18.0)(typescript@5.6.2) vite: specifier: ^6.0.5 - version: 6.0.11(terser@5.34.1)(tsx@4.19.1) + version: 6.0.11(@types/node@22.15.0)(terser@5.34.1)(tsx@4.19.1) + + examples/repro-07032025: + dependencies: + '@clockworklabs/spacetimedb-sdk': + specifier: workspace:* + version: link:../../packages/sdk + devDependencies: + '@types/node': + specifier: ^22.13.9 + version: 22.15.0 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.15.0)(typescript@5.8.3) + typescript: + specifier: ^5.8.2 + version: 5.8.3 + + examples/repro2: + dependencies: + '@clockworklabs/spacetimedb-sdk': + specifier: workspace:* + version: link:../../packages/sdk + devDependencies: + '@eslint/js': + specifier: ^9.17.0 + version: 9.18.0 + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.6.3 + '@testing-library/react': + specifier: ^16.2.0 + version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/user-event': + specifier: ^14.6.1 + version: 14.6.1(@testing-library/dom@10.4.0) + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/react': + specifier: ^18.3.18 + version: 18.3.18 + '@types/react-dom': + specifier: ^18.3.5 + version: 18.3.5(@types/react@18.3.18) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.3.4(vite@6.0.11(@types/node@22.15.0)(terser@5.34.1)(tsx@4.19.1)) + eslint: + specifier: ^9.17.0 + version: 9.18.0 + eslint-plugin-react-hooks: + specifier: ^5.0.0 + version: 5.1.0(eslint@9.18.0) + eslint-plugin-react-refresh: + specifier: ^0.4.16 + version: 0.4.18(eslint@9.18.0) + globals: + specifier: ^15.14.0 + version: 15.14.0 + jsdom: + specifier: ^26.0.0 + version: 26.0.0 + typescript: + specifier: ~5.6.2 + version: 5.6.2 + typescript-eslint: + specifier: ^8.18.2 + version: 8.21.0(eslint@9.18.0)(typescript@5.6.2) + vite: + specifier: ^6.0.5 + version: 6.0.11(@types/node@22.15.0)(terser@5.34.1)(tsx@4.19.1) packages/sdk: + dependencies: + base64-js: + specifier: ^1.5.1 + version: 1.5.1 devDependencies: '@clockworklabs/test-app': specifier: file:../test-app version: file:packages/test-app tsup: specifier: ^8.1.0 - version: 8.3.0(postcss@8.5.1)(tsx@4.19.1)(typescript@5.6.2) + version: 8.3.0(postcss@8.5.1)(tsx@4.19.1)(typescript@5.8.3) undici: specifier: ^6.19.2 version: 6.19.8 @@ -129,13 +204,13 @@ importers: version: 18.3.0 '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.3.2(vite@5.4.8(terser@5.34.1)) + version: 4.3.2(vite@5.4.8(@types/node@22.15.0)(terser@5.34.1)) typescript: specifier: ^5.2.2 version: 5.6.2 vite: specifier: ^5.3.4 - version: 5.4.8(terser@5.34.1) + version: 5.4.8(@types/node@22.15.0)(terser@5.34.1) packages: @@ -383,6 +458,10 @@ packages: '@clockworklabs/test-app@file:packages/test-app': resolution: {directory: packages/test-app, type: directory} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@csstools/color-helpers@5.0.1': resolution: {integrity: sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==} engines: {node: '>=18'} @@ -934,6 +1013,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -1068,6 +1150,18 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1104,6 +1198,9 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@22.15.0': + resolution: {integrity: sha512-99S8dWD2DkeE6PBaEDw+In3aar7hdoBvjyJMR6vaKBTzpvR0P00ClzJMOoVrj9D2+Sy/YCwACYHnBTpMhg1UCA==} + '@types/prop-types@15.7.13': resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} @@ -1224,6 +1321,10 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + acorn@8.12.1: resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} engines: {node: '>=0.4.0'} @@ -1276,6 +1377,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -1303,6 +1407,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -1420,6 +1527,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -1483,6 +1593,10 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1959,6 +2073,9 @@ packages: magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2486,6 +2603,20 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + tsup@8.3.0: resolution: {integrity: sha512-ALscEeyS03IomcuNdFdc0YWGVIkwH1Ws7nfTbAPuoILvEV2hpGQAY72LIOjglGo4ShWpZfpBqP/jpQVCzqYQag==} engines: {node: '>=18'} @@ -2526,6 +2657,14 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@6.19.8: resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==} engines: {node: '>=18.17'} @@ -2543,6 +2682,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + vite-node@2.1.2: resolution: {integrity: sha512-HPcGNN5g/7I2OtPjLqgOtCRu/qhVvBxTUD3qzitmL0SrG1cWFzxzhMDWussxSbrRYWqnKf8P2jiNhPMSN+ymsQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2727,6 +2869,10 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3149,6 +3295,10 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@csstools/color-helpers@5.0.1': {} '@csstools/css-calc@2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': @@ -3462,7 +3612,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 12.20.55 + '@types/node': 22.15.0 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -3488,6 +3638,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.25.7 @@ -3604,6 +3759,14 @@ snapshots: dependencies: '@testing-library/dom': 10.4.0 + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -3648,6 +3811,10 @@ snapshots: '@types/node@12.20.55': {} + '@types/node@22.15.0': + dependencies: + undici-types: 6.21.0 + '@types/prop-types@15.7.13': {} '@types/react-dom@18.3.0': @@ -3753,25 +3920,25 @@ snapshots: '@typescript-eslint/types': 8.21.0 eslint-visitor-keys: 4.2.0 - '@vitejs/plugin-react@4.3.2(vite@5.4.8(terser@5.34.1))': + '@vitejs/plugin-react@4.3.2(vite@5.4.8(@types/node@22.15.0)(terser@5.34.1))': dependencies: '@babel/core': 7.25.7 '@babel/plugin-transform-react-jsx-self': 7.25.7(@babel/core@7.25.7) '@babel/plugin-transform-react-jsx-source': 7.25.7(@babel/core@7.25.7) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.8(terser@5.34.1) + vite: 5.4.8(@types/node@22.15.0)(terser@5.34.1) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.3.4(vite@6.0.11(terser@5.34.1)(tsx@4.19.1))': + '@vitejs/plugin-react@4.3.4(vite@6.0.11(@types/node@22.15.0)(terser@5.34.1)(tsx@4.19.1))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.0.11(terser@5.34.1)(tsx@4.19.1) + vite: 6.0.11(@types/node@22.15.0)(terser@5.34.1)(tsx@4.19.1) transitivePeerDependencies: - supports-color @@ -3782,13 +3949,13 @@ snapshots: chai: 5.1.1 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(terser@5.34.1))': + '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.15.0)(terser@5.34.1))': dependencies: '@vitest/spy': 2.1.2 estree-walker: 3.0.3 magic-string: 0.30.11 optionalDependencies: - vite: 5.4.8(terser@5.34.1) + vite: 5.4.8(@types/node@22.15.0)(terser@5.34.1) '@vitest/pretty-format@2.1.2': dependencies: @@ -3819,6 +3986,10 @@ snapshots: dependencies: acorn: 8.14.0 + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.0 + acorn@8.12.1: {} acorn@8.14.0: {} @@ -3857,6 +4028,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + arg@4.1.3: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -3877,6 +4050,8 @@ snapshots: balanced-match@1.0.2: {} + base64-js@1.5.1: {} + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 @@ -3994,6 +4169,8 @@ snapshots: convert-source-map@2.0.0: {} + create-require@1.1.1: {} + cross-spawn@5.1.0: dependencies: lru-cache: 4.1.5 @@ -4046,6 +4223,8 @@ snapshots: diff-sequences@29.6.3: {} + diff@4.0.2: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -4495,7 +4674,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 12.20.55 + '@types/node': 22.15.0 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -4610,6 +4789,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + make-error@1.3.6: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -5052,6 +5233,24 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-node@10.9.2(@types/node@22.15.0)(typescript@5.8.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.15.0 + acorn: 8.14.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.8.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + tsup@8.3.0(postcss@8.5.1)(tsx@4.19.1)(typescript@5.6.2): dependencies: bundle-require: 5.0.0(esbuild@0.23.1) @@ -5079,6 +5278,33 @@ snapshots: - tsx - yaml + tsup@8.3.0(postcss@8.5.1)(tsx@4.19.1)(typescript@5.8.3): + dependencies: + bundle-require: 5.0.0(esbuild@0.23.1) + cac: 6.7.14 + chokidar: 3.6.0 + consola: 3.2.3 + debug: 4.3.7 + esbuild: 0.23.1 + execa: 5.1.1 + joycon: 3.1.1 + picocolors: 1.1.0 + postcss-load-config: 6.0.1(postcss@8.5.1)(tsx@4.19.1) + resolve-from: 5.0.0 + rollup: 4.24.0 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyglobby: 0.2.9 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.1 + typescript: 5.8.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsx@4.19.1: dependencies: esbuild: 0.23.1 @@ -5102,6 +5328,10 @@ snapshots: typescript@5.6.2: {} + typescript@5.8.3: {} + + undici-types@6.21.0: {} + undici@6.19.8: {} universalify@0.1.2: {} @@ -5116,12 +5346,14 @@ snapshots: dependencies: punycode: 2.3.1 - vite-node@2.1.2(terser@5.34.1): + v8-compile-cache-lib@3.0.1: {} + + vite-node@2.1.2(@types/node@22.15.0)(terser@5.34.1): dependencies: cac: 6.7.14 debug: 4.3.7 pathe: 1.1.2 - vite: 5.4.8(terser@5.34.1) + vite: 5.4.8(@types/node@22.15.0)(terser@5.34.1) transitivePeerDependencies: - '@types/node' - less @@ -5133,29 +5365,31 @@ snapshots: - supports-color - terser - vite@5.4.8(terser@5.34.1): + vite@5.4.8(@types/node@22.15.0)(terser@5.34.1): dependencies: esbuild: 0.21.5 postcss: 8.4.47 rollup: 4.24.0 optionalDependencies: + '@types/node': 22.15.0 fsevents: 2.3.3 terser: 5.34.1 - vite@6.0.11(terser@5.34.1)(tsx@4.19.1): + vite@6.0.11(@types/node@22.15.0)(terser@5.34.1)(tsx@4.19.1): dependencies: esbuild: 0.24.2 postcss: 8.5.1 rollup: 4.24.0 optionalDependencies: + '@types/node': 22.15.0 fsevents: 2.3.3 terser: 5.34.1 tsx: 4.19.1 - vitest@2.1.2(jsdom@26.0.0)(terser@5.34.1): + vitest@2.1.2(@types/node@22.15.0)(jsdom@26.0.0)(terser@5.34.1): dependencies: '@vitest/expect': 2.1.2 - '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(terser@5.34.1)) + '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.15.0)(terser@5.34.1)) '@vitest/pretty-format': 2.1.2 '@vitest/runner': 2.1.2 '@vitest/snapshot': 2.1.2 @@ -5170,10 +5404,11 @@ snapshots: tinyexec: 0.3.0 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.4.8(terser@5.34.1) - vite-node: 2.1.2(terser@5.34.1) + vite: 5.4.8(@types/node@22.15.0)(terser@5.34.1) + vite-node: 2.1.2(@types/node@22.15.0)(terser@5.34.1) why-is-node-running: 2.3.0 optionalDependencies: + '@types/node': 22.15.0 jsdom: 26.0.0 transitivePeerDependencies: - less @@ -5255,4 +5490,6 @@ snapshots: yallist@3.1.1: {} + yn@3.1.1: {} + yocto-queue@0.1.0: {} From bda80f067cdf0e15ed1aa5d8a50963a214e53d2c Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Fri, 25 Apr 2025 12:22:52 -0700 Subject: [PATCH 2/7] Use concat to avoid creating too many arguments for big updates. --- packages/sdk/src/db_connection_impl.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/db_connection_impl.ts b/packages/sdk/src/db_connection_impl.ts index 78d2d83e..2b7d6322 100644 --- a/packages/sdk/src/db_connection_impl.ts +++ b/packages/sdk/src/db_connection_impl.ts @@ -525,14 +525,15 @@ export class DbConnectionImpl< tableUpdates: TableUpdate[], eventContext: EventContextInterface ): PendingCallback[] { - const pendingCallbacks: PendingCallback[] = []; + let pendingCallbacks: PendingCallback[] = []; for (let tableUpdate of tableUpdates) { // Get table information for the table being updated const tableName = tableUpdate.tableName; const tableTypeInfo = this.#remoteModule.tables[tableName]!; const table = this.clientCache.getOrCreateTable(tableTypeInfo); - pendingCallbacks.push( - ...table.applyOperations(tableUpdate.operations, eventContext) + + pendingCallbacks = pendingCallbacks.concat( + table.applyOperations(tableUpdate.operations, eventContext) ); } return pendingCallbacks; From 31c83ee1254d3287818fa7f76f21bdcb1d5334b3 Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Fri, 25 Apr 2025 13:33:18 -0700 Subject: [PATCH 3/7] Use primitives as rowIds --- packages/sdk/src/algebraic_type.ts | 8 ++- packages/sdk/src/binary_writer.ts | 2 +- packages/sdk/src/db_connection_impl.ts | 27 ++++++--- packages/sdk/src/table_cache.ts | 84 ++++++++++++++------------ 4 files changed, 70 insertions(+), 51 deletions(-) diff --git a/packages/sdk/src/algebraic_type.ts b/packages/sdk/src/algebraic_type.ts index 35da90b4..09d89a30 100644 --- a/packages/sdk/src/algebraic_type.ts +++ b/packages/sdk/src/algebraic_type.ts @@ -164,7 +164,7 @@ export class ProductType { } }; - intoMapKey(value: any): any { + intoMapKey(value: any): ComparablePrimitive { if (this.elements.length === 1) { if (this.elements[0].name === '__time_duration_micros__') { return (value as TimeDuration).__time_duration_micros__; @@ -188,7 +188,7 @@ export class ProductType { return writer.toBase64(); } - deserialize = (reader: BinaryReader): any => { + deserialize = (reader: BinaryReader): { [key: string]: any } => { let result: { [key: string]: any } = {}; if (this.elements.length === 1) { if (this.elements[0].name === '__time_duration_micros__') { @@ -240,6 +240,8 @@ type AnyType = | TypeRef | None; +export type ComparablePrimitive = number | string | String | boolean | bigint; + /** * The SpacetimeDB Algebraic Type System (SATS) is a structural type system in * which a nominal type system can be constructed. @@ -480,7 +482,7 @@ export class AlgebraicType { * @param value A value of the algebraic type * @returns Something that can be used as a key in a map. */ - intoMapKey(value: any): any { + intoMapKey(value: any): ComparablePrimitive { switch (this.type) { case Type.U8: case Type.U16: diff --git a/packages/sdk/src/binary_writer.ts b/packages/sdk/src/binary_writer.ts index ec43c611..8a55a3ff 100644 --- a/packages/sdk/src/binary_writer.ts +++ b/packages/sdk/src/binary_writer.ts @@ -21,7 +21,7 @@ export default class BinaryWriter { this.#view = new DataView(this.#buffer.buffer); } - toBase64(): String { + toBase64(): string { return fromByteArray(this.#buffer.subarray(0, this.#offset)); } diff --git a/packages/sdk/src/db_connection_impl.ts b/packages/sdk/src/db_connection_impl.ts index 2b7d6322..65398946 100644 --- a/packages/sdk/src/db_connection_impl.ts +++ b/packages/sdk/src/db_connection_impl.ts @@ -309,21 +309,30 @@ export class DbConnectionImpl< const reader = new BinaryReader(buffer); const rows: Operation[] = []; const rowType = this.#remoteModule.tables[tableName]!.rowType; + const primaryKeyInfo = + this.#remoteModule.tables[tableName]!.primaryKeyInfo; while (reader.offset < buffer.length + buffer.byteOffset) { const initialOffset = reader.offset; const row = rowType.deserialize(reader); - - // Get a view of the bytes for this row. - const rowBytes = buffer.subarray( - initialOffset - buffer.byteOffset, - reader.offset - buffer.byteOffset - ); - // Convert it to a base64 string, so we can use it as a map key. - const asBase64 = fromByteArray(rowBytes); + let rowId: any | undefined = undefined; + if (primaryKeyInfo !== undefined) { + rowId = primaryKeyInfo.colType.intoMapKey( + row[primaryKeyInfo.colName] + ); + } else { + // Get a view of the bytes for this row. + const rowBytes = buffer.subarray( + initialOffset - buffer.byteOffset, + reader.offset - buffer.byteOffset + ); + // Convert it to a base64 string, so we can use it as a map key. + const asBase64 = fromByteArray(rowBytes); + rowId = asBase64; + } rows.push({ type, - rowId: asBase64, + rowId, row, }); } diff --git a/packages/sdk/src/table_cache.ts b/packages/sdk/src/table_cache.ts index ce6139db..ff70e4f4 100644 --- a/packages/sdk/src/table_cache.ts +++ b/packages/sdk/src/table_cache.ts @@ -7,11 +7,14 @@ import { type EventContextInterface, } from './db_connection_impl.ts'; import { stdbLogger } from './logger.ts'; +import type { ComparablePrimitive } from './algebraic_type.ts'; export type Operation = { type: 'insert' | 'delete'; - // rowId: string; - rowId: string; + // For tables with a primary key, this is the primary key value, as a primitive or string. + // Otherwise, it is an encoding of the full row. + rowId: ComparablePrimitive; + // TODO: Refine this type to at least reflect that it is a product. row: any; }; @@ -29,7 +32,7 @@ export type PendingCallback = { * Builder to generate calls to query a `table` in the database */ export class TableCache { - private rows: Map; + private rows: Map; private tableTypeInfo: TableRuntimeTypeInfo; private emitter: EventEmitter<'insert' | 'delete' | 'update'>; @@ -65,30 +68,18 @@ export class TableCache { ): PendingCallback[] => { const pendingCallbacks: PendingCallback[] = []; if (this.tableTypeInfo.primaryKeyInfo !== undefined) { - const primaryKeyCol = this.tableTypeInfo.primaryKeyInfo.colName; - const primaryKeyType = this.tableTypeInfo.primaryKeyInfo.colType; - const getPrimaryKey = (row: any) => { - const primaryKeyValue = row[primaryKeyCol]; - const writer = new BinaryWriter(10); - primaryKeyType.serialize(writer, primaryKeyValue); - return writer.toBase64(); - }; - const insertMap = new OperationsMap(); - const deleteMap = new OperationsMap(); + const insertMap = new Map(); + const deleteMap = new Map(); for (const op of operations) { - const primaryKey = getPrimaryKey(op.row); if (op.type === 'insert') { - const [_, prevCount] = insertMap.get(primaryKey) || [op, 0]; - insertMap.set(primaryKey, [op, prevCount + 1]); + const [_, prevCount] = insertMap.get(op.rowId) || [op, 0]; + insertMap.set(op.rowId, [op, prevCount + 1]); } else { - const [_, prevCount] = deleteMap.get(primaryKey) || [op, 0]; - deleteMap.set(primaryKey, [op, prevCount + 1]); + const [_, prevCount] = deleteMap.get(op.rowId) || [op, 0]; + deleteMap.set(op.rowId, [op, prevCount + 1]); } } - for (const { - key: primaryKey, - value: [insertOp, refCount], - } of insertMap) { + for (const [primaryKey, [insertOp, refCount]] of insertMap) { const deleteEntry = deleteMap.get(primaryKey); if (deleteEntry) { const [deleteOp, deleteCount] = deleteEntry; @@ -96,7 +87,12 @@ export class TableCache { // an update moves a row in or out of the result set of different queries, then // other deltas are possible. const refCountDelta = refCount - deleteCount; - const maybeCb = this.update(ctx, insertOp, deleteOp, refCountDelta); + const maybeCb = this.update( + ctx, + primaryKey, + insertOp.row, + refCountDelta + ); if (maybeCb) { pendingCallbacks.push(maybeCb); } @@ -134,36 +130,48 @@ export class TableCache { update = ( ctx: EventContextInterface, - newDbOp: Operation, - oldDbOp: Operation, + rowId: ComparablePrimitive, + newRow: RowType, refCountDelta: number = 0 ): PendingCallback | undefined => { - const [oldRow, previousCount] = this.rows.get(oldDbOp.rowId) || [ - oldDbOp.row, - 0, - ]; + const existingEntry = this.rows.get(rowId); + if (!existingEntry) { + // TODO: this should throw an error and kill the connection. + stdbLogger( + 'error', + `Updating a row that was not present in the cache. Table: ${this.tableTypeInfo.tableName}, RowId: ${rowId}` + ); + return undefined; + } + const [oldRow, previousCount] = existingEntry; const refCount = Math.max(1, previousCount + refCountDelta); - this.rows.delete(oldDbOp.rowId); - this.rows.set(newDbOp.rowId, [newDbOp.row, refCount]); + if (previousCount + refCountDelta <= 0) { + stdbLogger( + 'error', + `Negative reference count for in table ${this.tableTypeInfo.tableName} row ${rowId} (${previousCount} + ${refCountDelta})` + ); + return undefined; + } + this.rows.set(rowId, [newRow, refCount]); // This indicates something is wrong, so we could arguably crash here. if (previousCount === 0) { - stdbLogger('error', 'Updating a row that was not present in the cache'); + stdbLogger( + 'error', + `Updating a row id in table ${this.tableTypeInfo.tableName} which was not present in the cache (rowId: ${rowId})` + ); return { type: 'insert', table: this.tableTypeInfo.tableName, cb: () => { - this.emitter.emit('insert', ctx, newDbOp.row); + this.emitter.emit('insert', ctx, newRow); }, }; - } else if (previousCount + refCountDelta <= 0) { - stdbLogger('error', 'Negative reference count for row'); - // TODO: We should actually error and kill the connection here. } return { type: 'update', table: this.tableTypeInfo.tableName, cb: () => { - this.emitter.emit('update', ctx, oldRow, newDbOp.row); + this.emitter.emit('update', ctx, oldRow, newRow); }, }; }; @@ -187,7 +195,7 @@ export class TableCache { }, }; } - console.log(`previousCount of ${previousCount} for ${operation.rowId}`); + // It's possible to get a duplicate insert because rows can be returned from multiple queries. return undefined; }; From 5534ace9e74ac32f0f2a2158d2313a5e10a17634 Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Fri, 25 Apr 2025 14:17:29 -0700 Subject: [PATCH 4/7] Remove OperationsMap --- packages/sdk/src/operations_map.ts | 63 ------------------------------ packages/sdk/src/table_cache.ts | 1 - 2 files changed, 64 deletions(-) delete mode 100644 packages/sdk/src/operations_map.ts diff --git a/packages/sdk/src/operations_map.ts b/packages/sdk/src/operations_map.ts deleted file mode 100644 index 6ba22f47..00000000 --- a/packages/sdk/src/operations_map.ts +++ /dev/null @@ -1,63 +0,0 @@ -export default class OperationsMap { - #items: { key: K; value: V }[] = []; - - #isEqual(a: K, b: K): boolean { - if (a && typeof a === 'object' && 'isEqual' in a) { - return (a as any).isEqual(b); - } - return a === b; - } - - set(key: K, value: V): void { - const existingIndex = this.#items.findIndex(({ key: k }) => - this.#isEqual(k, key) - ); - if (existingIndex > -1) { - this.#items[existingIndex].value = value; - } else { - this.#items.push({ key, value }); - } - } - - get(key: K): V | undefined { - const item = this.#items.find(({ key: k }) => this.#isEqual(k, key)); - return item ? item.value : undefined; - } - - delete(key: K): boolean { - const existingIndex = this.#items.findIndex(({ key: k }) => - this.#isEqual(k, key) - ); - if (existingIndex > -1) { - this.#items.splice(existingIndex, 1); - return true; - } - return false; - } - - has(key: K): boolean { - return this.#items.some(({ key: k }) => this.#isEqual(k, key)); - } - - values(): Array { - return this.#items.map(i => i.value); - } - - entries(): Array<{ key: K; value: V }> { - return this.#items; - } - - [Symbol.iterator](): Iterator<{ key: K; value: V }> { - let index = 0; - const items = this.#items; - return { - next(): IteratorResult<{ key: K; value: V }> { - if (index < items.length) { - return { value: items[index++], done: false }; - } else { - return { value: null, done: true }; - } - }, - }; - } -} diff --git a/packages/sdk/src/table_cache.ts b/packages/sdk/src/table_cache.ts index ff70e4f4..270a6cd6 100644 --- a/packages/sdk/src/table_cache.ts +++ b/packages/sdk/src/table_cache.ts @@ -1,5 +1,4 @@ import { EventEmitter } from './event_emitter.ts'; -import OperationsMap from './operations_map.ts'; import type { TableRuntimeTypeInfo } from './spacetime_module.ts'; import { From 4ca7a0490499dda5f04c2ca8741fe065f4b631db Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Wed, 30 Apr 2025 08:43:43 -0700 Subject: [PATCH 5/7] Add some table cache tests and regen stuff. --- .../identity_connected_reducer.ts | 2 + .../identity_disconnected_reducer.ts | 2 + .../src/module_bindings/index.ts | 10 +- .../src/module_bindings/message_table.ts | 2 + .../src/module_bindings/message_type.ts | 2 + .../module_bindings/send_message_reducer.ts | 2 + .../src/module_bindings/set_name_reducer.ts | 2 + .../src/module_bindings/user_table.ts | 2 + .../src/module_bindings/user_type.ts | 2 + packages/sdk/src/db_connection_impl.ts | 46 +- packages/sdk/src/table_cache.ts | 2 +- packages/sdk/tests/table_cache.test.ts | 782 ++++++++++++++++++ packages/test-app/server/Cargo.toml | 3 +- packages/test-app/server/src/lib.rs | 7 + packages/test-app/src/main.tsx | 6 +- .../test-app/src/module_bindings/index.ts | 16 + .../module_bindings/unindexed_player_table.ts | 78 ++ .../module_bindings/unindexed_player_type.ts | 67 ++ 18 files changed, 1003 insertions(+), 30 deletions(-) create mode 100644 packages/sdk/tests/table_cache.test.ts create mode 100644 packages/test-app/src/module_bindings/unindexed_player_table.ts create mode 100644 packages/test-app/src/module_bindings/unindexed_player_type.ts diff --git a/examples/quickstart-chat/src/module_bindings/identity_connected_reducer.ts b/examples/quickstart-chat/src/module_bindings/identity_connected_reducer.ts index b7221cfe..c915a4f0 100644 --- a/examples/quickstart-chat/src/module_bindings/identity_connected_reducer.ts +++ b/examples/quickstart-chat/src/module_bindings/identity_connected_reducer.ts @@ -1,6 +1,8 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. +// This was generated using spacetimedb cli version 1.1.1 (commit e2c36856906f0d365d76d4e633bac6d84755eb2f). + /* eslint-disable */ /* tslint:disable */ // @ts-nocheck diff --git a/examples/quickstart-chat/src/module_bindings/identity_disconnected_reducer.ts b/examples/quickstart-chat/src/module_bindings/identity_disconnected_reducer.ts index 60225304..66d33b62 100644 --- a/examples/quickstart-chat/src/module_bindings/identity_disconnected_reducer.ts +++ b/examples/quickstart-chat/src/module_bindings/identity_disconnected_reducer.ts @@ -1,6 +1,8 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. +// This was generated using spacetimedb cli version 1.1.1 (commit e2c36856906f0d365d76d4e633bac6d84755eb2f). + /* eslint-disable */ /* tslint:disable */ // @ts-nocheck diff --git a/examples/quickstart-chat/src/module_bindings/index.ts b/examples/quickstart-chat/src/module_bindings/index.ts index 698f69f3..39a2f23a 100644 --- a/examples/quickstart-chat/src/module_bindings/index.ts +++ b/examples/quickstart-chat/src/module_bindings/index.ts @@ -1,6 +1,8 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. +// This was generated using spacetimedb cli version 1.1.1 (commit e2c36856906f0d365d76d4e633bac6d84755eb2f). + /* eslint-disable */ /* tslint:disable */ // @ts-nocheck @@ -63,11 +65,6 @@ const REMOTE_MODULE = { tableName: 'user', rowType: User.getTypeScriptAlgebraicType(), primaryKey: 'identity', - primaryKeyInfo: { - colName: 'identity', - colType: - User.getTypeScriptAlgebraicType().product.elements[0].algebraicType, - }, }, }, reducers: { @@ -88,6 +85,9 @@ const REMOTE_MODULE = { argsType: SetName.getTypeScriptAlgebraicType(), }, }, + versionInfo: { + cliVersion: '1.1.1', + }, // Constructors which are used by the DbConnectionImpl to // extract type information from the generated RemoteModule. // diff --git a/examples/quickstart-chat/src/module_bindings/message_table.ts b/examples/quickstart-chat/src/module_bindings/message_table.ts index 34b4661b..024e552f 100644 --- a/examples/quickstart-chat/src/module_bindings/message_table.ts +++ b/examples/quickstart-chat/src/module_bindings/message_table.ts @@ -1,6 +1,8 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. +// This was generated using spacetimedb cli version 1.1.1 (commit e2c36856906f0d365d76d4e633bac6d84755eb2f). + /* eslint-disable */ /* tslint:disable */ // @ts-nocheck diff --git a/examples/quickstart-chat/src/module_bindings/message_type.ts b/examples/quickstart-chat/src/module_bindings/message_type.ts index e65b080f..849fddf4 100644 --- a/examples/quickstart-chat/src/module_bindings/message_type.ts +++ b/examples/quickstart-chat/src/module_bindings/message_type.ts @@ -1,6 +1,8 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. +// This was generated using spacetimedb cli version 1.1.1 (commit e2c36856906f0d365d76d4e633bac6d84755eb2f). + /* eslint-disable */ /* tslint:disable */ // @ts-nocheck diff --git a/examples/quickstart-chat/src/module_bindings/send_message_reducer.ts b/examples/quickstart-chat/src/module_bindings/send_message_reducer.ts index c3b14af5..c85eaf36 100644 --- a/examples/quickstart-chat/src/module_bindings/send_message_reducer.ts +++ b/examples/quickstart-chat/src/module_bindings/send_message_reducer.ts @@ -1,6 +1,8 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. +// This was generated using spacetimedb cli version 1.1.1 (commit e2c36856906f0d365d76d4e633bac6d84755eb2f). + /* eslint-disable */ /* tslint:disable */ // @ts-nocheck diff --git a/examples/quickstart-chat/src/module_bindings/set_name_reducer.ts b/examples/quickstart-chat/src/module_bindings/set_name_reducer.ts index 8d2be56e..2be66889 100644 --- a/examples/quickstart-chat/src/module_bindings/set_name_reducer.ts +++ b/examples/quickstart-chat/src/module_bindings/set_name_reducer.ts @@ -1,6 +1,8 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. +// This was generated using spacetimedb cli version 1.1.1 (commit e2c36856906f0d365d76d4e633bac6d84755eb2f). + /* eslint-disable */ /* tslint:disable */ // @ts-nocheck diff --git a/examples/quickstart-chat/src/module_bindings/user_table.ts b/examples/quickstart-chat/src/module_bindings/user_table.ts index 1a918b9c..4d328189 100644 --- a/examples/quickstart-chat/src/module_bindings/user_table.ts +++ b/examples/quickstart-chat/src/module_bindings/user_table.ts @@ -1,6 +1,8 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. +// This was generated using spacetimedb cli version 1.1.1 (commit e2c36856906f0d365d76d4e633bac6d84755eb2f). + /* eslint-disable */ /* tslint:disable */ // @ts-nocheck diff --git a/examples/quickstart-chat/src/module_bindings/user_type.ts b/examples/quickstart-chat/src/module_bindings/user_type.ts index 074b537a..fca8d7cb 100644 --- a/examples/quickstart-chat/src/module_bindings/user_type.ts +++ b/examples/quickstart-chat/src/module_bindings/user_type.ts @@ -1,6 +1,8 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. +// This was generated using spacetimedb cli version 1.1.1 (commit e2c36856906f0d365d76d4e633bac6d84755eb2f). + /* eslint-disable */ /* tslint:disable */ // @ts-nocheck diff --git a/packages/sdk/src/db_connection_impl.ts b/packages/sdk/src/db_connection_impl.ts index 65398946..a9d02527 100644 --- a/packages/sdk/src/db_connection_impl.ts +++ b/packages/sdk/src/db_connection_impl.ts @@ -15,7 +15,13 @@ import { } from './algebraic_value.ts'; import BinaryReader from './binary_reader.ts'; import BinaryWriter from './binary_writer.ts'; -import * as ws from './client_api/index.ts'; +import { BsatnRowList } from './client_api/bsatn_row_list_type.ts'; +import { ClientMessage } from './client_api/client_message_type.ts'; +import { DatabaseUpdate } from './client_api/database_update_type.ts'; +import { QueryUpdate } from './client_api/query_update_type.ts'; +import { ServerMessage } from './client_api/server_message_type.ts'; +import { TableUpdate as RawTableUpdate } from './client_api/table_update_type.ts'; +import type * as clientApi from './client_api/index.ts'; import { ClientCache } from './client_cache.ts'; import { DbConnectionBuilder } from './db_connection_builder.ts'; import { type DbContext } from './db_context.ts'; @@ -41,7 +47,7 @@ import { TableCache, type Operation, type PendingCallback, - type TableUpdate, + type TableUpdate as CacheTableUpdate, } from './table_cache.ts'; import { deepEqual, toPascalCase } from './utils.ts'; import { WebsocketDecompressAdapter } from './websocket_decompress_adapter.ts'; @@ -53,7 +59,7 @@ import { type SubscribeEvent, } from './subscription_builder_impl.ts'; import { stdbLogger } from './logger.ts'; -import type { ReducerRuntimeTypeInfo } from './spacetime_module.ts'; +import { type ReducerRuntimeTypeInfo } from './spacetime_module.ts'; import { fromByteArray } from 'base64-js'; export { @@ -274,7 +280,7 @@ export class DbConnectionImpl< emitter: handleEmitter, }); this.#sendMessage( - ws.ClientMessage.SubscribeMulti({ + ClientMessage.SubscribeMulti({ queryStrings: querySql, queryId: { id: queryId }, // The TypeScript SDK doesn't currently track `request_id`s, @@ -287,7 +293,7 @@ export class DbConnectionImpl< unregisterSubscription(queryId: number): void { this.#sendMessage( - ws.ClientMessage.UnsubscribeMulti({ + ClientMessage.UnsubscribeMulti({ queryId: { id: queryId }, // The TypeScript SDK doesn't currently track `request_id`s, // so always use 0. @@ -298,12 +304,12 @@ export class DbConnectionImpl< // This function is async because we decompress the message async async #processParsedMessage( - message: ws.ServerMessage + message: ServerMessage ): Promise { const parseRowList = ( type: 'insert' | 'delete', tableName: string, - rowList: ws.BsatnRowList + rowList: BsatnRowList ): Operation[] => { const buffer = rowList.rowsData; const reader = new BinaryReader(buffer); @@ -340,15 +346,15 @@ export class DbConnectionImpl< }; const parseTableUpdate = async ( - rawTableUpdate: ws.TableUpdate - ): Promise => { + rawTableUpdate: RawTableUpdate + ): Promise => { const tableName = rawTableUpdate.tableName; let operations: Operation[] = []; for (const update of rawTableUpdate.updates) { - let decompressed: ws.QueryUpdate; + let decompressed: QueryUpdate; if (update.tag === 'Gzip') { const decompressedBuffer = await decompress(update.value, 'gzip'); - decompressed = ws.QueryUpdate.deserialize( + decompressed = QueryUpdate.deserialize( new BinaryReader(decompressedBuffer) ); } else if (update.tag === 'Brotli') { @@ -372,9 +378,9 @@ export class DbConnectionImpl< }; const parseDatabaseUpdate = async ( - dbUpdate: ws.DatabaseUpdate - ): Promise => { - const tableUpdates: TableUpdate[] = []; + dbUpdate: DatabaseUpdate + ): Promise => { + const tableUpdates: CacheTableUpdate[] = []; for (const rawTableUpdate of dbUpdate.tables) { tableUpdates.push(await parseTableUpdate(rawTableUpdate)); } @@ -412,7 +418,7 @@ export class DbConnectionImpl< const args = txUpdate.reducerCall.args; const energyQuantaUsed = txUpdate.energyQuantaUsed; - let tableUpdates: TableUpdate[]; + let tableUpdates: CacheTableUpdate[]; let errMessage = ''; switch (txUpdate.status.tag) { case 'Committed': @@ -512,11 +518,11 @@ export class DbConnectionImpl< } } - #sendMessage(message: ws.ClientMessage): void { + #sendMessage(message: ClientMessage): void { this.wsPromise.then(wsResolved => { if (wsResolved) { const writer = new BinaryWriter(1024); - ws.ClientMessage.serialize(writer, message); + ClientMessage.serialize(writer, message); const encoded = writer.getBuffer(); wsResolved.send(encoded); } @@ -531,7 +537,7 @@ export class DbConnectionImpl< } #applyTableUpdates( - tableUpdates: TableUpdate[], + tableUpdates: CacheTableUpdate[], eventContext: EventContextInterface ): PendingCallback[] { let pendingCallbacks: PendingCallback[] = []; @@ -549,7 +555,7 @@ export class DbConnectionImpl< } async #processMessage(data: Uint8Array): Promise { - const serverMessage = parseValue(ws.ServerMessage, data); + const serverMessage = parseValue(ServerMessage, data); const message = await this.#processParsedMessage(serverMessage); if (!message) { return; @@ -803,7 +809,7 @@ export class DbConnectionImpl< argsBuffer: Uint8Array, flags: CallReducerFlags ): void { - const message = ws.ClientMessage.CallReducer({ + const message = ClientMessage.CallReducer({ reducer: reducerName, args: argsBuffer, // The TypeScript SDK doesn't currently track `request_id`s, diff --git a/packages/sdk/src/table_cache.ts b/packages/sdk/src/table_cache.ts index 270a6cd6..8763c218 100644 --- a/packages/sdk/src/table_cache.ts +++ b/packages/sdk/src/table_cache.ts @@ -81,7 +81,7 @@ export class TableCache { for (const [primaryKey, [insertOp, refCount]] of insertMap) { const deleteEntry = deleteMap.get(primaryKey); if (deleteEntry) { - const [deleteOp, deleteCount] = deleteEntry; + const [_, deleteCount] = deleteEntry; // In most cases the refCountDelta will be either 0 or refCount, but if // an update moves a row in or out of the result set of different queries, then // other deltas are possible. diff --git a/packages/sdk/tests/table_cache.test.ts b/packages/sdk/tests/table_cache.test.ts new file mode 100644 index 00000000..1e666af4 --- /dev/null +++ b/packages/sdk/tests/table_cache.test.ts @@ -0,0 +1,782 @@ +import { Operation, TableCache } from '../src/table_cache'; +import type { TableRuntimeTypeInfo } from '../src/spacetime_module'; +import { describe, expect, test } from 'vitest'; + +import { Player } from '@clockworklabs/test-app/src/module_bindings'; + +import { AlgebraicType, ProductTypeElement } from '../src/algebraic_type'; + +interface ApplyOperations { + ops: Operation[]; + ctx: any; +} + +interface CallbackEvent { + type: 'insert' | 'delete' | 'update'; + ctx: any; + row: any; + oldRow?: any; // Only there for updates. +} + +function insertEvent(row: any, ctx: any = {}): CallbackEvent { + return { + type: 'insert', + ctx, + row, + }; +} + +function updateEvent(oldRow: any, row: any, ctx: any = {}): CallbackEvent { + return { + type: 'update', + ctx, + row, + oldRow, + }; +} + +function deleteEvent(row: any, ctx: any = {}): CallbackEvent { + return { + type: 'delete', + ctx, + row, + }; +} + +interface AssertionInput { + // The state of the table cache. + tableCache: TableCache; + // The sequence of callbacks that were fired from the last applyOperations. + callbackHistory: CallbackEvent[]; +} + +type Assertion = (AssertionInput) => void; + +interface TestStep { + // The operations to apply. + ops: ApplyOperations; + // The assertions to make after applying the operations. + assertions: Assertion[]; +} + +function runTest(tableCache: TableCache, testSteps: TestStep[]) { + const callbackHistory: CallbackEvent[] = []; + tableCache.onInsert((ctx, row) => { + callbackHistory.push({ + type: 'insert', + ctx, + row, + }); + }); + tableCache.onDelete((ctx, row) => { + callbackHistory.push({ + type: 'delete', + ctx, + row, + }); + }); + tableCache.onUpdate((ctx, oldRow, row) => { + callbackHistory.push({ + type: 'update', + ctx, + row, + oldRow, + }); + }); + + for (const step of testSteps) { + const { ops: applyOperations, assertions } = step; + const { ops, ctx } = applyOperations; + const callbacks = tableCache.applyOperations(ops, ctx); + callbacks.forEach(cb => cb.cb()); + for (const assertion of assertions) { + assertion({ tableCache, callbackHistory }); + } + // Clear the callback history for the next step. + callbackHistory.length = 0; + } +} + +describe('TableCache', () => { + describe('Unindexed player table', () => { + const pointType = AlgebraicType.createProductType([ + new ProductTypeElement('x', AlgebraicType.createU16Type()), + new ProductTypeElement('y', AlgebraicType.createU16Type()), + ]); + const playerType = AlgebraicType.createProductType([ + new ProductTypeElement('ownerId', AlgebraicType.createStringType()), + new ProductTypeElement('name', AlgebraicType.createStringType()), + new ProductTypeElement('location', pointType), + ]); + const tableTypeInfo: TableRuntimeTypeInfo = { + tableName: 'player', + rowType: playerType, + }; + const newTable = () => new TableCache(tableTypeInfo); + const mkOperation = (type: 'insert' | 'delete', row: Player) => { + let rowId = tableTypeInfo.rowType.intoMapKey(row); + return { + type, + rowId, + row, + }; + }; + + test('Insert one', () => { + const tableCache = newTable(); + const steps: TestStep[] = []; + let player = { + ownerId: '1', + name: 'Player 1', + location: { + x: 1, + y: 2, + }, + }; + steps.push({ + ops: { + ops: [mkOperation('insert', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter().length).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory.length).toBe(1); + expect(callbackHistory[0].type).toBe('insert'); + expect(callbackHistory[0].row).toEqual(player); + }, + ], + }); + runTest(tableCache, steps); + }); + + test('Inserting one twice only triggers one event', () => { + const tableCache = newTable(); + const steps: TestStep[] = []; + let player = { + ownerId: '1', + name: 'Player 1', + location: { + x: 1, + y: 2, + }, + }; + steps.push({ + ops: { + ops: [mkOperation('insert', player), mkOperation('insert', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter().length).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory.length).toBe(1); + expect(callbackHistory[0].type).toBe('insert'); + expect(callbackHistory[0].row).toEqual(player); + }, + ], + }); + runTest(tableCache, steps); + }); + + test('Insert dupe is a noop', () => { + const tableCache = newTable(); + const steps: TestStep[] = []; + let player = { + ownerId: '1', + name: 'Player 1', + location: { + x: 1, + y: 2, + }, + }; + steps.push({ + ops: { + ops: [mkOperation('insert', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter().length).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory.length).toBe(1); + expect(callbackHistory[0].type).toBe('insert'); + expect(callbackHistory[0].row).toEqual(player); + }, + ], + }); + steps.push({ + ops: { + ops: [mkOperation('insert', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter().length).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory.length).toBe(0); + }, + ], + }); + runTest(tableCache, steps); + }); + + test('Insert once and delete', () => { + const tableCache = newTable(); + const steps: TestStep[] = []; + let player = { + ownerId: '1', + name: 'Player 1', + location: { + x: 1, + y: 2, + }, + }; + steps.push({ + ops: { + ops: [mkOperation('insert', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory.length).toBe(1); + expect(callbackHistory[0].type).toBe('insert'); + expect(callbackHistory[0].row).toEqual(player); + }, + ], + }); + steps.push({ + ops: { + ops: [mkOperation('delete', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(0); + expect(callbackHistory.length).toBe(1); + expect(callbackHistory[0].type).toBe('delete'); + expect(callbackHistory[0].row).toEqual(player); + }, + ], + }); + runTest(tableCache, steps); + }); + + test('Insert twice and delete', () => { + const tableCache = newTable(); + const steps: TestStep[] = []; + let mkPlayer = () => ({ + ownerId: '1', + name: 'Player 1', + location: { + x: 1, + y: 2, + }, + }); + let player = { + ownerId: '1', + name: 'Player 1', + location: { + x: 1, + y: 2, + }, + }; + steps.push({ + ops: { + ops: [ + mkOperation('insert', mkPlayer()), + mkOperation('insert', player), + ], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory.length).toBe(1); + expect(callbackHistory[0].type).toBe('insert'); + expect(callbackHistory[0].row).toEqual(player); + }, + ], + }); + steps.push({ + ops: { + ops: [mkOperation('delete', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + // We still have one reference left, so it isn't actually deleted. + expect(tableCache.count()).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory.length).toBe(0); + }, + ], + }); + steps.push({ + ops: { + ops: [mkOperation('delete', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + // Now it is actually deleted. + expect(tableCache.count()).toBe(0); + expect(callbackHistory.length).toBe(1); + expect(callbackHistory[0].type).toBe('delete'); + expect(callbackHistory[0].row).toEqual(player); + }, + ], + }); + // Now we are going to insert again, so we can delete both refs at once. + steps.push({ + ops: { + ops: [mkOperation('insert', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory).toEqual([insertEvent(player)]); + }, + ], + }); + steps.push({ + ops: { + ops: [mkOperation('insert', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory).toEqual([]); + }, + ], + }); + steps.push({ + ops: { + ops: [mkOperation('delete', player), mkOperation('delete', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(0); + expect(callbackHistory).toEqual([deleteEvent(mkPlayer())]); + }, + ], + }); + runTest(tableCache, steps); + }); + + test('Insert one', () => { + const tableCache = newTable(); + let op = mkOperation('insert', { + ownerId: '1', + name: 'Player 1', + location: { + x: 1, + y: 2, + }, + }); + let rowsInserted = 0; + let callbacks = tableCache.applyOperations([op], {} as any); + tableCache.onInsert((ctx, row) => { + rowsInserted++; + expect(row).toEqual(op.row); + }); + expect(callbacks.length).toBe(1); + expect(tableCache.count()).toBe(1); + callbacks.forEach(cb => { + cb.cb(); + }); + expect(rowsInserted).toBe(1); + }); + }); + describe('Indexed player table', () => { + const pointType = AlgebraicType.createProductType([ + new ProductTypeElement('x', AlgebraicType.createU16Type()), + new ProductTypeElement('y', AlgebraicType.createU16Type()), + ]); + const playerType = AlgebraicType.createProductType([ + new ProductTypeElement('ownerId', AlgebraicType.createStringType()), + new ProductTypeElement('name', AlgebraicType.createStringType()), + new ProductTypeElement('location', pointType), + ]); + const tableTypeInfo: TableRuntimeTypeInfo = { + tableName: 'player', + rowType: playerType, + primaryKeyInfo: { + colName: 'ownerId', + colType: playerType.product.elements[0].algebraicType, + }, + }; + const newTable = () => new TableCache(tableTypeInfo); + const mkOperation = (type: 'insert' | 'delete', row: Player) => { + let rowId = tableTypeInfo.primaryKeyInfo!.colType.intoMapKey( + row['ownerId'] + ); + return { + type, + rowId, + row, + }; + }; + + test('Insert one', () => { + const tableCache = newTable(); + const steps: TestStep[] = []; + let player = { + ownerId: '1', + name: 'Player 1', + location: { + x: 1, + y: 2, + }, + }; + steps.push({ + ops: { + ops: [mkOperation('insert', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter().length).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory.length).toBe(1); + expect(callbackHistory[0].type).toBe('insert'); + expect(callbackHistory[0].row).toEqual(player); + }, + ], + }); + runTest(tableCache, steps); + }); + + test('Inserting one twice only triggers one event', () => { + const tableCache = newTable(); + const steps: TestStep[] = []; + let player = { + ownerId: '1', + name: 'Player 1', + location: { + x: 1, + y: 2, + }, + }; + steps.push({ + ops: { + ops: [mkOperation('insert', player), mkOperation('insert', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter().length).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory.length).toBe(1); + expect(callbackHistory[0].type).toBe('insert'); + expect(callbackHistory[0].row).toEqual(player); + }, + ], + }); + runTest(tableCache, steps); + }); + + test('Insert dupe is a noop', () => { + const tableCache = newTable(); + const steps: TestStep[] = []; + let player = { + ownerId: '1', + name: 'Player 1', + location: { + x: 1, + y: 2, + }, + }; + steps.push({ + ops: { + ops: [mkOperation('insert', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter().length).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory.length).toBe(1); + expect(callbackHistory[0].type).toBe('insert'); + expect(callbackHistory[0].row).toEqual(player); + }, + ], + }); + steps.push({ + ops: { + ops: [mkOperation('insert', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter().length).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory.length).toBe(0); + }, + ], + }); + runTest(tableCache, steps); + }); + + test('Insert once and delete', () => { + const tableCache = newTable(); + const steps: TestStep[] = []; + let player = { + ownerId: '1', + name: 'Player 1', + location: { + x: 1, + y: 2, + }, + }; + steps.push({ + ops: { + ops: [mkOperation('insert', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory.length).toBe(1); + expect(callbackHistory[0].type).toBe('insert'); + expect(callbackHistory[0].row).toEqual(player); + }, + ], + }); + steps.push({ + ops: { + ops: [mkOperation('delete', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(0); + expect(callbackHistory.length).toBe(1); + expect(callbackHistory[0].type).toBe('delete'); + expect(callbackHistory[0].row).toEqual(player); + }, + ], + }); + runTest(tableCache, steps); + }); + + test('Update smoke test', () => { + const tableCache = newTable(); + const steps: TestStep[] = []; + let mkPlayer = (name: string) => ({ + ownerId: '1', + name: name, + location: { + x: 1, + y: 2, + }, + }); + steps.push({ + ops: { + ops: [mkOperation('insert', mkPlayer('jeff'))], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter()[0]).toEqual(mkPlayer('jeff')); + expect(callbackHistory).toEqual([insertEvent(mkPlayer('jeff'))]); + }, + ], + }); + steps.push({ + ops: { + ops: [ + mkOperation('delete', mkPlayer('jeff')), + mkOperation('insert', mkPlayer('jeffv2')), + ], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter()[0]).toEqual(mkPlayer('jeffv2')); + expect(callbackHistory).toEqual([ + updateEvent(mkPlayer('jeff'), mkPlayer('jeffv2')), + ]); + }, + ], + }); + runTest(tableCache, steps); + }); + + test('Insert twice and delete', () => { + const tableCache = newTable(); + const steps: TestStep[] = []; + let mkPlayer = () => ({ + ownerId: '1', + name: 'Player 1', + location: { + x: 1, + y: 2, + }, + }); + let player = { + ownerId: '1', + name: 'Player 1', + location: { + x: 1, + y: 2, + }, + }; + steps.push({ + ops: { + ops: [ + mkOperation('insert', mkPlayer()), + mkOperation('insert', player), + ], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory.length).toBe(1); + expect(callbackHistory[0].type).toBe('insert'); + expect(callbackHistory[0].row).toEqual(player); + }, + ], + }); + steps.push({ + ops: { + ops: [mkOperation('delete', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + // We still have one reference left, so it isn't actually deleted. + expect(tableCache.count()).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory.length).toBe(0); + }, + ], + }); + steps.push({ + ops: { + ops: [mkOperation('delete', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + // Now it is actually deleted. + expect(tableCache.count()).toBe(0); + expect(callbackHistory.length).toBe(1); + expect(callbackHistory[0].type).toBe('delete'); + expect(callbackHistory[0].row).toEqual(player); + }, + ], + }); + // Now we are going to insert again, so we can delete both refs at once. + steps.push({ + ops: { + ops: [mkOperation('insert', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory).toEqual([insertEvent(player)]); + }, + ], + }); + steps.push({ + ops: { + ops: [mkOperation('insert', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(1); + expect(tableCache.iter()[0]).toEqual(player); + expect(callbackHistory).toEqual([]); + }, + ], + }); + steps.push({ + ops: { + ops: [mkOperation('delete', player), mkOperation('delete', player)], + ctx: {} as any, + }, + assertions: [ + ({ tableCache, callbackHistory }) => { + expect(tableCache.count()).toBe(0); + expect(callbackHistory).toEqual([deleteEvent(mkPlayer())]); + }, + ], + }); + runTest(tableCache, steps); + }); + + test('Insert one', () => { + const tableCache = newTable(); + let op = mkOperation('insert', { + ownerId: '1', + name: 'Player 1', + location: { + x: 1, + y: 2, + }, + }); + let rowsInserted = 0; + let callbacks = tableCache.applyOperations([op], {} as any); + tableCache.onInsert((ctx, row) => { + rowsInserted++; + expect(row).toEqual(op.row); + }); + expect(callbacks.length).toBe(1); + expect(tableCache.count()).toBe(1); + callbacks.forEach(cb => { + cb.cb(); + }); + expect(rowsInserted).toBe(1); + }); + }); + + const pointType = AlgebraicType.createProductType([ + new ProductTypeElement('x', AlgebraicType.createU16Type()), + new ProductTypeElement('y', AlgebraicType.createU16Type()), + ]); + const playerType = AlgebraicType.createProductType([ + new ProductTypeElement('ownerId', AlgebraicType.createStringType()), + new ProductTypeElement('name', AlgebraicType.createStringType()), + new ProductTypeElement('location', pointType), + ]); + + test('should be empty on creation', () => { + const tableTypeInfo: TableRuntimeTypeInfo = { + tableName: 'player', + rowType: playerType, + primaryKeyInfo: { + colName: 'ownerId', + colType: playerType.product.elements[0].algebraicType, + }, + }; + const tableCache = new TableCache(tableTypeInfo); + expect(tableCache.count()).toBe(0); + tableCache.applyOperations; + }); +}); diff --git a/packages/test-app/server/Cargo.toml b/packages/test-app/server/Cargo.toml index 907abb1f..a66b6126 100644 --- a/packages/test-app/server/Cargo.toml +++ b/packages/test-app/server/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -spacetimedb = { path = "../../../../SpacetimeDB/crates/bindings" } +spacetimedb = { path = "/Users/jeffreydallatezza/st/SpacetimeDBPrivate/public/crates/bindings" } +# spacetimedb = "1.1.1" log = "0.4" anyhow = "1.0" diff --git a/packages/test-app/server/src/lib.rs b/packages/test-app/server/src/lib.rs index 14550c0a..4fbd2086 100644 --- a/packages/test-app/server/src/lib.rs +++ b/packages/test-app/server/src/lib.rs @@ -21,6 +21,13 @@ pub struct User { pub username: String, } +#[table(name = unindexed_player, public)] +pub struct UnindexedPlayer { + owner_id: String, + name: String, + location: Point, +} + #[reducer] pub fn create_player(ctx: &ReducerContext, name: String, location: Point) { ctx.db.player().insert(Player { diff --git a/packages/test-app/src/main.tsx b/packages/test-app/src/main.tsx index fef10819..93db3799 100644 --- a/packages/test-app/src/main.tsx +++ b/packages/test-app/src/main.tsx @@ -4,7 +4,7 @@ import App from './App.tsx'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( - // - - // + + + ); diff --git a/packages/test-app/src/module_bindings/index.ts b/packages/test-app/src/module_bindings/index.ts index 07395d11..646857cc 100644 --- a/packages/test-app/src/module_bindings/index.ts +++ b/packages/test-app/src/module_bindings/index.ts @@ -38,6 +38,8 @@ export { CreatePlayer }; // Import and reexport all table handle types import { PlayerTableHandle } from './player_table.ts'; export { PlayerTableHandle }; +import { UnindexedPlayerTableHandle } from './unindexed_player_table.ts'; +export { UnindexedPlayerTableHandle }; import { UserTableHandle } from './user_table.ts'; export { UserTableHandle }; @@ -46,6 +48,8 @@ import { Player } from './player_type.ts'; export { Player }; import { Point } from './point_type.ts'; export { Point }; +import { UnindexedPlayer } from './unindexed_player_type.ts'; +export { UnindexedPlayer }; import { User } from './user_type.ts'; export { User }; @@ -61,6 +65,10 @@ const REMOTE_MODULE = { Player.getTypeScriptAlgebraicType().product.elements[0].algebraicType, }, }, + unindexed_player: { + tableName: 'unindexed_player', + rowType: UnindexedPlayer.getTypeScriptAlgebraicType(), + }, user: { tableName: 'user', rowType: User.getTypeScriptAlgebraicType(), @@ -157,6 +165,14 @@ export class RemoteTables { ); } + get unindexedPlayer(): UnindexedPlayerTableHandle { + return new UnindexedPlayerTableHandle( + this.connection.clientCache.getOrCreateTable( + REMOTE_MODULE.tables.unindexed_player + ) + ); + } + get user(): UserTableHandle { return new UserTableHandle( this.connection.clientCache.getOrCreateTable( diff --git a/packages/test-app/src/module_bindings/unindexed_player_table.ts b/packages/test-app/src/module_bindings/unindexed_player_table.ts new file mode 100644 index 00000000..a9045964 --- /dev/null +++ b/packages/test-app/src/module_bindings/unindexed_player_table.ts @@ -0,0 +1,78 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +import { + AlgebraicType, + AlgebraicValue, + BinaryReader, + BinaryWriter, + CallReducerFlags, + ConnectionId, + DbConnectionBuilder, + DbConnectionImpl, + DbContext, + ErrorContextInterface, + Event, + EventContextInterface, + Identity, + ProductType, + ProductTypeElement, + ReducerEventContextInterface, + SubscriptionBuilderImpl, + SubscriptionEventContextInterface, + SumType, + SumTypeVariant, + TableCache, + TimeDuration, + Timestamp, + deepEqual, +} from '@clockworklabs/spacetimedb-sdk'; +import { UnindexedPlayer } from './unindexed_player_type'; +import { Point as __Point } from './point_type'; + +import { EventContext, Reducer, RemoteReducers, RemoteTables } from '.'; + +/** + * Table handle for the table `unindexed_player`. + * + * Obtain a handle from the [`unindexedPlayer`] property on [`RemoteTables`], + * like `ctx.db.unindexedPlayer`. + * + * Users are encouraged not to explicitly reference this type, + * but to directly chain method calls, + * like `ctx.db.unindexedPlayer.on_insert(...)`. + */ +export class UnindexedPlayerTableHandle { + tableCache: TableCache; + + constructor(tableCache: TableCache) { + this.tableCache = tableCache; + } + + count(): number { + return this.tableCache.count(); + } + + iter(): Iterable { + return this.tableCache.iter(); + } + + onInsert = (cb: (ctx: EventContext, row: UnindexedPlayer) => void) => { + return this.tableCache.onInsert(cb); + }; + + removeOnInsert = (cb: (ctx: EventContext, row: UnindexedPlayer) => void) => { + return this.tableCache.removeOnInsert(cb); + }; + + onDelete = (cb: (ctx: EventContext, row: UnindexedPlayer) => void) => { + return this.tableCache.onDelete(cb); + }; + + removeOnDelete = (cb: (ctx: EventContext, row: UnindexedPlayer) => void) => { + return this.tableCache.removeOnDelete(cb); + }; +} diff --git a/packages/test-app/src/module_bindings/unindexed_player_type.ts b/packages/test-app/src/module_bindings/unindexed_player_type.ts new file mode 100644 index 00000000..5ee83c9e --- /dev/null +++ b/packages/test-app/src/module_bindings/unindexed_player_type.ts @@ -0,0 +1,67 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +import { + AlgebraicType, + AlgebraicValue, + BinaryReader, + BinaryWriter, + CallReducerFlags, + ConnectionId, + DbConnectionBuilder, + DbConnectionImpl, + DbContext, + ErrorContextInterface, + Event, + EventContextInterface, + Identity, + ProductType, + ProductTypeElement, + ReducerEventContextInterface, + SubscriptionBuilderImpl, + SubscriptionEventContextInterface, + SumType, + SumTypeVariant, + TableCache, + TimeDuration, + Timestamp, + deepEqual, +} from '@clockworklabs/spacetimedb-sdk'; +import { Point as __Point } from './point_type'; + +export type UnindexedPlayer = { + ownerId: string; + name: string; + location: __Point; +}; + +/** + * A namespace for generated helper functions. + */ +export namespace UnindexedPlayer { + /** + * A function which returns this type represented as an AlgebraicType. + * This function is derived from the AlgebraicType used to generate this type. + */ + export function getTypeScriptAlgebraicType(): AlgebraicType { + return AlgebraicType.createProductType([ + new ProductTypeElement('ownerId', AlgebraicType.createStringType()), + new ProductTypeElement('name', AlgebraicType.createStringType()), + new ProductTypeElement('location', __Point.getTypeScriptAlgebraicType()), + ]); + } + + export function serialize( + writer: BinaryWriter, + value: UnindexedPlayer + ): void { + UnindexedPlayer.getTypeScriptAlgebraicType().serialize(writer, value); + } + + export function deserialize(reader: BinaryReader): UnindexedPlayer { + return UnindexedPlayer.getTypeScriptAlgebraicType().deserialize(reader); + } +} From 9064e7cbfb2b5589bc200ff014e3965efb5edeff Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Tue, 6 May 2025 11:57:26 -0700 Subject: [PATCH 6/7] Cleanup --- packages/sdk/src/db_connection_impl.ts | 12 ++++++++---- packages/sdk/src/spacetime_module.ts | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/sdk/src/db_connection_impl.ts b/packages/sdk/src/db_connection_impl.ts index a9d02527..65cf317e 100644 --- a/packages/sdk/src/db_connection_impl.ts +++ b/packages/sdk/src/db_connection_impl.ts @@ -5,6 +5,7 @@ import { ProductTypeElement, SumType, SumTypeVariant, + type ComparablePrimitive, } from './algebraic_type.ts'; import { AlgebraicValue, @@ -320,7 +321,7 @@ export class DbConnectionImpl< while (reader.offset < buffer.length + buffer.byteOffset) { const initialOffset = reader.offset; const row = rowType.deserialize(reader); - let rowId: any | undefined = undefined; + let rowId: ComparablePrimitive | undefined = undefined; if (primaryKeyInfo !== undefined) { rowId = primaryKeyInfo.colType.intoMapKey( row[primaryKeyInfo.colName] @@ -546,10 +547,13 @@ export class DbConnectionImpl< const tableName = tableUpdate.tableName; const tableTypeInfo = this.#remoteModule.tables[tableName]!; const table = this.clientCache.getOrCreateTable(tableTypeInfo); - - pendingCallbacks = pendingCallbacks.concat( - table.applyOperations(tableUpdate.operations, eventContext) + const newCallbacks = table.applyOperations( + tableUpdate.operations, + eventContext ); + for (const callback of newCallbacks) { + pendingCallbacks.push(callback); + } } return pendingCallbacks; } diff --git a/packages/sdk/src/spacetime_module.ts b/packages/sdk/src/spacetime_module.ts index 6ffb61e8..1a214ad8 100644 --- a/packages/sdk/src/spacetime_module.ts +++ b/packages/sdk/src/spacetime_module.ts @@ -4,7 +4,7 @@ import type { DbConnectionImpl } from './db_connection_impl'; export interface TableRuntimeTypeInfo { tableName: string; rowType: AlgebraicType; - primaryKeyInfo?: PrimaryKeyInfo | undefined; + primaryKeyInfo?: PrimaryKeyInfo; } export interface PrimaryKeyInfo { From b8116f892e203708c8f232eed74863deda8b9134 Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Tue, 6 May 2025 11:58:25 -0700 Subject: [PATCH 7/7] revert accidental cargo change. --- packages/test-app/server/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/test-app/server/Cargo.toml b/packages/test-app/server/Cargo.toml index a66b6126..907abb1f 100644 --- a/packages/test-app/server/Cargo.toml +++ b/packages/test-app/server/Cargo.toml @@ -9,7 +9,6 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -spacetimedb = { path = "/Users/jeffreydallatezza/st/SpacetimeDBPrivate/public/crates/bindings" } -# spacetimedb = "1.1.1" +spacetimedb = { path = "../../../../SpacetimeDB/crates/bindings" } log = "0.4" anyhow = "1.0"