spacetimedb 2.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +2 -2
- package/dist/angular/index.cjs +5 -1
- package/dist/angular/index.cjs.map +1 -1
- package/dist/angular/index.mjs +5 -1
- package/dist/angular/index.mjs.map +1 -1
- package/dist/browser/angular/index.mjs +5 -1
- package/dist/browser/angular/index.mjs.map +1 -1
- package/dist/browser/react/index.mjs +8 -1
- package/dist/browser/react/index.mjs.map +1 -1
- package/dist/browser/svelte/index.mjs +5 -1
- package/dist/browser/svelte/index.mjs.map +1 -1
- package/dist/browser/vue/index.mjs +5 -1
- package/dist/browser/vue/index.mjs.map +1 -1
- package/dist/index.browser.mjs +148 -100
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.cjs +148 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +148 -100
- package/dist/index.mjs.map +1 -1
- package/dist/lib/algebraic_type.d.ts.map +1 -1
- package/dist/lib/binary_writer.d.ts +1 -0
- package/dist/lib/binary_writer.d.ts.map +1 -1
- package/dist/lib/indexes.d.ts +1 -1
- package/dist/lib/indexes.d.ts.map +1 -1
- package/dist/lib/query.d.ts +14 -7
- package/dist/lib/query.d.ts.map +1 -1
- package/dist/lib/schema.d.ts +2 -0
- package/dist/lib/schema.d.ts.map +1 -1
- package/dist/lib/table.d.ts +25 -2
- package/dist/lib/table.d.ts.map +1 -1
- package/dist/min/index.browser.mjs +1 -1
- package/dist/min/index.browser.mjs.map +1 -1
- package/dist/min/react/index.mjs +1 -1
- package/dist/min/react/index.mjs.map +1 -1
- package/dist/min/sdk/index.browser.mjs +1 -1
- package/dist/min/sdk/index.browser.mjs.map +1 -1
- package/dist/react/index.cjs +8 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.mjs +8 -1
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/useTable.d.ts.map +1 -1
- package/dist/sdk/db_connection_impl.d.ts.map +1 -1
- package/dist/sdk/index.browser.mjs +144 -98
- package/dist/sdk/index.browser.mjs.map +1 -1
- package/dist/sdk/index.cjs +144 -98
- package/dist/sdk/index.cjs.map +1 -1
- package/dist/sdk/index.mjs +144 -98
- package/dist/sdk/index.mjs.map +1 -1
- package/dist/sdk/table_cache.d.ts.map +1 -1
- package/dist/sdk/websocket_decompress_adapter.d.ts +17 -7
- package/dist/sdk/websocket_decompress_adapter.d.ts.map +1 -1
- package/dist/sdk/websocket_test_adapter.d.ts +3 -2
- package/dist/sdk/websocket_test_adapter.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.mjs +88 -30
- package/dist/server/index.mjs.map +1 -1
- package/dist/svelte/index.cjs +5 -1
- package/dist/svelte/index.cjs.map +1 -1
- package/dist/svelte/index.mjs +5 -1
- package/dist/svelte/index.mjs.map +1 -1
- package/dist/tanstack/SpacetimeDBQueryClient.d.ts +1 -0
- package/dist/tanstack/SpacetimeDBQueryClient.d.ts.map +1 -1
- package/dist/tanstack/index.cjs +26 -1
- package/dist/tanstack/index.cjs.map +1 -1
- package/dist/tanstack/index.mjs +26 -1
- package/dist/tanstack/index.mjs.map +1 -1
- package/dist/vue/index.cjs +5 -1
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.mjs +5 -1
- package/dist/vue/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/lib/algebraic_type.ts +5 -1
- package/src/lib/binary_writer.ts +4 -0
- package/src/lib/indexes.ts +1 -1
- package/src/lib/query.ts +90 -25
- package/src/lib/schema.ts +66 -24
- package/src/lib/table.ts +47 -10
- package/src/react/useTable.ts +5 -0
- package/src/sdk/db_connection_impl.ts +38 -43
- package/src/sdk/table_cache.ts +14 -11
- package/src/sdk/websocket_decompress_adapter.ts +42 -45
- package/src/sdk/websocket_test_adapter.ts +3 -2
- package/src/server/index.ts +1 -0
- package/src/server/runtime.ts +7 -3
- package/src/server/schema.test-d.ts +37 -0
- package/src/server/view.test-d.ts +6 -0
- package/src/tanstack/SpacetimeDBQueryClient.ts +24 -0
package/package.json
CHANGED
|
@@ -535,7 +535,11 @@ const view = reader.view;
|
|
|
535
535
|
${ty.elements
|
|
536
536
|
.map(({ name, algebraicType: { tag } }) =>
|
|
537
537
|
tag in primitiveJSName
|
|
538
|
-
?
|
|
538
|
+
? tag === 'Bool'
|
|
539
|
+
? `\
|
|
540
|
+
result.${name} = view.getUint8(reader.offset) !== 0;
|
|
541
|
+
reader.offset += 1;`
|
|
542
|
+
: `\
|
|
539
543
|
result.${name} = view.get${primitiveJSName[tag as JSPrimitives]}(reader.offset, ${primitiveSizes[tag] > 1 ? 'true' : ''});
|
|
540
544
|
reader.offset += ${primitiveSizes[tag]};`
|
|
541
545
|
: `result.${name} = reader.read${tag}();`
|
package/src/lib/binary_writer.ts
CHANGED
package/src/lib/indexes.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { ColumnIsUnique } from './constraints';
|
|
|
9
9
|
* existing column names are referenced.
|
|
10
10
|
*/
|
|
11
11
|
export type IndexOpts<AllowedCol extends string> = {
|
|
12
|
-
accessor
|
|
12
|
+
accessor: string;
|
|
13
13
|
name?: string;
|
|
14
14
|
} & (
|
|
15
15
|
| { algorithm: 'btree'; columns: readonly AllowedCol[] }
|
package/src/lib/query.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
TypeBuilder,
|
|
12
12
|
} from './type_builders';
|
|
13
13
|
import type { Values } from './type_util';
|
|
14
|
+
import type { Bool as SatsBool } from './algebraic_type_variants';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Helper to get the set of table names.
|
|
@@ -65,7 +66,7 @@ type From<TableDef extends TypedTableDef> = RowTypedQuery<
|
|
|
65
66
|
Readonly<{
|
|
66
67
|
toSql(): string;
|
|
67
68
|
where(
|
|
68
|
-
predicate: (row: RowExpr<TableDef>) =>
|
|
69
|
+
predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
|
|
69
70
|
): From<TableDef>;
|
|
70
71
|
rightSemijoin<RightTable extends TypedTableDef>(
|
|
71
72
|
other: TableRef<RightTable>,
|
|
@@ -93,7 +94,7 @@ type SemijoinBuilder<TableDef extends TypedTableDef> = RowTypedQuery<
|
|
|
93
94
|
Readonly<{
|
|
94
95
|
toSql(): string;
|
|
95
96
|
where(
|
|
96
|
-
predicate: (row: RowExpr<TableDef>) =>
|
|
97
|
+
predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
|
|
97
98
|
): SemijoinBuilder<TableDef>;
|
|
98
99
|
/** @deprecated No longer needed — builder is already a valid query. */
|
|
99
100
|
build(): Query<TableDef>;
|
|
@@ -120,7 +121,7 @@ class SemijoinImpl<TableDef extends TypedTableDef>
|
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
where(
|
|
123
|
-
predicate: (row: RowExpr<TableDef>) =>
|
|
124
|
+
predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
|
|
124
125
|
): SemijoinImpl<TableDef> {
|
|
125
126
|
const nextSourceQuery = this.sourceQuery.where(predicate);
|
|
126
127
|
return new SemijoinImpl<TableDef>(
|
|
@@ -167,9 +168,9 @@ class FromBuilder<TableDef extends TypedTableDef>
|
|
|
167
168
|
) {}
|
|
168
169
|
|
|
169
170
|
where(
|
|
170
|
-
predicate: (row: RowExpr<TableDef>) =>
|
|
171
|
+
predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
|
|
171
172
|
): FromBuilder<TableDef> {
|
|
172
|
-
const newCondition = predicate(this.table.cols);
|
|
173
|
+
const newCondition = normalizePredicateExpr(predicate(this.table.cols));
|
|
173
174
|
const nextWhere = this.whereClause
|
|
174
175
|
? this.whereClause.and(newCondition)
|
|
175
176
|
: newCondition;
|
|
@@ -308,7 +309,7 @@ class TableRefImpl<TableDef extends TypedTableDef>
|
|
|
308
309
|
}
|
|
309
310
|
|
|
310
311
|
where(
|
|
311
|
-
predicate: (row: RowExpr<TableDef>) =>
|
|
312
|
+
predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
|
|
312
313
|
): FromBuilder<TableDef> {
|
|
313
314
|
return this.asFrom().where(predicate);
|
|
314
315
|
}
|
|
@@ -347,7 +348,8 @@ function createRowExpr<TableDef extends TypedTableDef>(
|
|
|
347
348
|
columnBuilder.typeBuilder.algebraicType as InferSpacetimeTypeOfColumn<
|
|
348
349
|
TableDef,
|
|
349
350
|
typeof columnName
|
|
350
|
-
|
|
351
|
+
>,
|
|
352
|
+
columnBuilder.columnMetadata.name
|
|
351
353
|
);
|
|
352
354
|
row[columnName] = Object.freeze(column);
|
|
353
355
|
}
|
|
@@ -437,7 +439,10 @@ export class ColumnExpression<
|
|
|
437
439
|
ColumnName extends ColumnNames<TableDef>,
|
|
438
440
|
> {
|
|
439
441
|
readonly type = 'column' as const;
|
|
442
|
+
// This is the column accessor
|
|
440
443
|
readonly column: ColumnName;
|
|
444
|
+
// The name of the column in the database.
|
|
445
|
+
readonly columnName: string;
|
|
441
446
|
readonly table: TableDef['sourceName'];
|
|
442
447
|
// phantom: actual runtime value is undefined
|
|
443
448
|
readonly tsValueType?: RowType<TableDef>[ColumnName];
|
|
@@ -446,10 +451,12 @@ export class ColumnExpression<
|
|
|
446
451
|
constructor(
|
|
447
452
|
table: TableDef['sourceName'],
|
|
448
453
|
column: ColumnName,
|
|
449
|
-
spacetimeType: InferSpacetimeTypeOfColumn<TableDef, ColumnName
|
|
454
|
+
spacetimeType: InferSpacetimeTypeOfColumn<TableDef, ColumnName>,
|
|
455
|
+
columnName?: string
|
|
450
456
|
) {
|
|
451
457
|
this.table = table;
|
|
452
458
|
this.column = column;
|
|
459
|
+
this.columnName = columnName || column;
|
|
453
460
|
this.spacetimeType = spacetimeType;
|
|
454
461
|
}
|
|
455
462
|
|
|
@@ -628,6 +635,11 @@ export type ValueExpr<TableDef extends TypedTableDef, Value> =
|
|
|
628
635
|
| LiteralExpr<Value & LiteralValue>
|
|
629
636
|
| ColumnExprForValue<TableDef, Value>;
|
|
630
637
|
|
|
638
|
+
type PredicateExpr<TableDef extends TypedTableDef> =
|
|
639
|
+
| BooleanExpr<TableDef>
|
|
640
|
+
| ColumnExprForValue<TableDef, SatsBool>
|
|
641
|
+
| boolean;
|
|
642
|
+
|
|
631
643
|
type LiteralExpr<Value> = {
|
|
632
644
|
type: 'literal';
|
|
633
645
|
value: Value;
|
|
@@ -654,6 +666,24 @@ function normalizeValue(val: ValueInput<any>): ValueExpr<any, any> {
|
|
|
654
666
|
return literal(val as LiteralValue);
|
|
655
667
|
}
|
|
656
668
|
|
|
669
|
+
function normalizePredicateExpr<TableDef extends TypedTableDef>(
|
|
670
|
+
value: PredicateExpr<TableDef>
|
|
671
|
+
): BooleanExpr<TableDef> {
|
|
672
|
+
if (value instanceof BooleanExpr) return value;
|
|
673
|
+
if (typeof value === 'boolean') {
|
|
674
|
+
return new BooleanExpr({
|
|
675
|
+
type: 'eq',
|
|
676
|
+
left: literal(value),
|
|
677
|
+
right: literal(true),
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
return new BooleanExpr({
|
|
681
|
+
type: 'eq',
|
|
682
|
+
left: value as ValueExpr<TableDef, any>,
|
|
683
|
+
right: literal(true),
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
657
687
|
type EqExpr<Table extends TypedTableDef = any> = BooleanExpr<Table>;
|
|
658
688
|
|
|
659
689
|
type BooleanExprData<Table extends TypedTableDef> = (
|
|
@@ -686,15 +716,38 @@ type BooleanExprData<Table extends TypedTableDef> = (
|
|
|
686
716
|
_tableType?: Table;
|
|
687
717
|
};
|
|
688
718
|
|
|
719
|
+
type AndOrMixedTableScopeError = {
|
|
720
|
+
readonly 'Cannot combine predicates from different table scopes with and/or. In semijoin on(...), keep only the join equality and move extra predicates to .where(...).': never;
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
type RequireSameAndOrTable<
|
|
724
|
+
Expected extends TypedTableDef,
|
|
725
|
+
Actual extends TypedTableDef,
|
|
726
|
+
> = [Expected] extends [Actual]
|
|
727
|
+
? [Actual] extends [Expected]
|
|
728
|
+
? unknown
|
|
729
|
+
: AndOrMixedTableScopeError
|
|
730
|
+
: AndOrMixedTableScopeError;
|
|
731
|
+
|
|
689
732
|
export class BooleanExpr<Table extends TypedTableDef> {
|
|
690
733
|
constructor(readonly data: BooleanExprData<Table>) {}
|
|
691
734
|
|
|
692
|
-
and
|
|
693
|
-
|
|
735
|
+
and<OtherTable extends TypedTableDef>(
|
|
736
|
+
other: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>
|
|
737
|
+
): BooleanExpr<Table> {
|
|
738
|
+
return new BooleanExpr({
|
|
739
|
+
type: 'and',
|
|
740
|
+
clauses: [this.data, other.data as BooleanExprData<Table>],
|
|
741
|
+
});
|
|
694
742
|
}
|
|
695
743
|
|
|
696
|
-
or
|
|
697
|
-
|
|
744
|
+
or<OtherTable extends TypedTableDef>(
|
|
745
|
+
other: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>
|
|
746
|
+
): BooleanExpr<Table> {
|
|
747
|
+
return new BooleanExpr({
|
|
748
|
+
type: 'or',
|
|
749
|
+
clauses: [this.data, other.data as BooleanExprData<Table>],
|
|
750
|
+
});
|
|
698
751
|
}
|
|
699
752
|
|
|
700
753
|
not(): BooleanExpr<Table> {
|
|
@@ -708,28 +761,40 @@ export function not<T extends TypedTableDef>(
|
|
|
708
761
|
return new BooleanExpr({ type: 'not', clause: clause.data });
|
|
709
762
|
}
|
|
710
763
|
|
|
711
|
-
export function and<
|
|
712
|
-
|
|
713
|
-
|
|
764
|
+
export function and<
|
|
765
|
+
Table extends TypedTableDef,
|
|
766
|
+
OtherTable extends TypedTableDef,
|
|
767
|
+
>(
|
|
768
|
+
first: BooleanExpr<Table>,
|
|
769
|
+
second: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>,
|
|
770
|
+
...rest: readonly BooleanExpr<Table>[]
|
|
771
|
+
): BooleanExpr<Table> {
|
|
772
|
+
const clauses = [first, second, ...rest];
|
|
714
773
|
return new BooleanExpr({
|
|
715
774
|
type: 'and',
|
|
716
775
|
clauses: clauses.map(c => c.data) as [
|
|
717
|
-
BooleanExprData<
|
|
718
|
-
BooleanExprData<
|
|
719
|
-
...BooleanExprData<
|
|
776
|
+
BooleanExprData<Table>,
|
|
777
|
+
BooleanExprData<Table>,
|
|
778
|
+
...BooleanExprData<Table>[],
|
|
720
779
|
],
|
|
721
780
|
});
|
|
722
781
|
}
|
|
723
782
|
|
|
724
|
-
export function or<
|
|
725
|
-
|
|
726
|
-
|
|
783
|
+
export function or<
|
|
784
|
+
Table extends TypedTableDef,
|
|
785
|
+
OtherTable extends TypedTableDef,
|
|
786
|
+
>(
|
|
787
|
+
first: BooleanExpr<Table>,
|
|
788
|
+
second: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>,
|
|
789
|
+
...rest: readonly BooleanExpr<Table>[]
|
|
790
|
+
): BooleanExpr<Table> {
|
|
791
|
+
const clauses = [first, second, ...rest];
|
|
727
792
|
return new BooleanExpr({
|
|
728
793
|
type: 'or',
|
|
729
794
|
clauses: clauses.map(c => c.data) as [
|
|
730
|
-
BooleanExprData<
|
|
731
|
-
BooleanExprData<
|
|
732
|
-
...BooleanExprData<
|
|
795
|
+
BooleanExprData<Table>,
|
|
796
|
+
BooleanExprData<Table>,
|
|
797
|
+
...BooleanExprData<Table>[],
|
|
733
798
|
],
|
|
734
799
|
});
|
|
735
800
|
}
|
|
@@ -779,7 +844,7 @@ function valueExprToSql<Table extends TypedTableDef>(
|
|
|
779
844
|
return literalValueToSql(expr.value);
|
|
780
845
|
}
|
|
781
846
|
const table = tableAlias ?? expr.table;
|
|
782
|
-
return `${quoteIdentifier(table)}.${quoteIdentifier(expr.
|
|
847
|
+
return `${quoteIdentifier(table)}.${quoteIdentifier(expr.columnName)}`;
|
|
783
848
|
}
|
|
784
849
|
|
|
785
850
|
function literalValueToSql(value: unknown): string {
|
package/src/lib/schema.ts
CHANGED
|
@@ -61,20 +61,35 @@ export interface TableToSchema<
|
|
|
61
61
|
accessorName: AccName;
|
|
62
62
|
columns: T['rowType']['row'];
|
|
63
63
|
rowType: T['rowSpacetimeType'];
|
|
64
|
+
// Declarative user-provided table-level indexes.
|
|
64
65
|
indexes: T['idxs'];
|
|
66
|
+
// Resolved runtime index metadata used by runtime consumers (e.g. TableCache).
|
|
67
|
+
resolvedIndexes: readonly UntypedIndex<keyof T['rowType']['row'] & string>[];
|
|
65
68
|
constraints: T['constraints'];
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
export function tablesToSchema<
|
|
69
72
|
const T extends Record<string, UntypedTableSchema>,
|
|
70
73
|
>(ctx: ModuleContext, tables: T): TablesToSchema<T> {
|
|
74
|
+
// `TablesToSchema<T>['tables']` is intentionally readonly in the public type,
|
|
75
|
+
// but we need a mutable builder while materializing it from entries.
|
|
76
|
+
type MutableTableDefs = {
|
|
77
|
+
-readonly [AccName in keyof TablesToSchema<T>['tables']]: TablesToSchema<T>['tables'][AccName];
|
|
78
|
+
};
|
|
79
|
+
const tableDefs = Object.create(null) as MutableTableDefs;
|
|
80
|
+
for (const [accName, schema] of Object.entries(tables) as [
|
|
81
|
+
keyof T & string,
|
|
82
|
+
T[keyof T & string],
|
|
83
|
+
][]) {
|
|
84
|
+
tableDefs[accName] = tableToSchema(
|
|
85
|
+
accName,
|
|
86
|
+
schema,
|
|
87
|
+
schema.tableDef(ctx, accName)
|
|
88
|
+
) as TablesToSchema<T>['tables'][typeof accName];
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
return {
|
|
72
|
-
tables:
|
|
73
|
-
Object.entries(tables).map(([accName, schema]) => [
|
|
74
|
-
accName,
|
|
75
|
-
tableToSchema(accName, schema, schema.tableDef(ctx, accName)),
|
|
76
|
-
])
|
|
77
|
-
) as TablesToSchema<T>['tables'],
|
|
92
|
+
tables: tableDefs as TablesToSchema<T>['tables'],
|
|
78
93
|
};
|
|
79
94
|
}
|
|
80
95
|
|
|
@@ -90,6 +105,46 @@ export function tableToSchema<
|
|
|
90
105
|
schema.rowType.algebraicType.value.elements[i].name;
|
|
91
106
|
|
|
92
107
|
type AllowedCol = keyof T['rowType']['row'] & string;
|
|
108
|
+
// Build fully-resolved runtime index metadata from the host-facing RawTableDef.
|
|
109
|
+
// This is intentionally separate from `schema.idxs`, which keeps the original
|
|
110
|
+
// user-declared `IndexOpts` shape for type-level inference.
|
|
111
|
+
const resolvedIndexes: UntypedIndex<AllowedCol>[] = tableDef.indexes.map(
|
|
112
|
+
idx => {
|
|
113
|
+
const accessorName = idx.accessorName;
|
|
114
|
+
if (typeof accessorName !== 'string' || accessorName.length === 0) {
|
|
115
|
+
throw new TypeError(
|
|
116
|
+
`Index '${idx.sourceName ?? '<unknown>'}' on table '${tableDef.sourceName}' is missing accessor name`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const columnIds =
|
|
121
|
+
idx.algorithm.tag === 'Direct'
|
|
122
|
+
? [idx.algorithm.value]
|
|
123
|
+
: idx.algorithm.value;
|
|
124
|
+
|
|
125
|
+
const unique = tableDef.constraints.some(
|
|
126
|
+
c =>
|
|
127
|
+
c.data.tag === 'Unique' &&
|
|
128
|
+
c.data.value.columns.every(col => columnIds.includes(col))
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const algorithm = (
|
|
132
|
+
{
|
|
133
|
+
BTree: 'btree',
|
|
134
|
+
Hash: 'hash',
|
|
135
|
+
Direct: 'direct',
|
|
136
|
+
} as const
|
|
137
|
+
)[idx.algorithm.tag];
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
name: accessorName,
|
|
141
|
+
unique,
|
|
142
|
+
algorithm,
|
|
143
|
+
columns: columnIds.map(getColName) as AllowedCol[],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
|
|
93
148
|
return {
|
|
94
149
|
// For client,`schama.tableName` will always be there as canonical name.
|
|
95
150
|
// For module, if explicit name is not provided via `name`, accessor name will
|
|
@@ -98,29 +153,16 @@ export function tableToSchema<
|
|
|
98
153
|
accessorName: accName,
|
|
99
154
|
columns: schema.rowType.row, // typed as T[i]['rowType']['row'] under TablesToSchema<T>
|
|
100
155
|
rowType: schema.rowSpacetimeType,
|
|
156
|
+
// Keep declarative indexes in their original shape for type-level consumers.
|
|
157
|
+
indexes: schema.idxs,
|
|
101
158
|
constraints: tableDef.constraints.map(c => ({
|
|
102
159
|
name: c.sourceName,
|
|
103
160
|
constraint: 'unique',
|
|
104
161
|
columns: c.data.value.columns.map(getColName) as [string],
|
|
105
162
|
})),
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
// We should stop lying about our types.
|
|
110
|
-
indexes: tableDef.indexes.map((idx): UntypedIndex<AllowedCol> => {
|
|
111
|
-
const columnIds =
|
|
112
|
-
idx.algorithm.tag === 'Direct'
|
|
113
|
-
? [idx.algorithm.value]
|
|
114
|
-
: idx.algorithm.value;
|
|
115
|
-
return {
|
|
116
|
-
name: idx.accessorName!,
|
|
117
|
-
unique: tableDef.constraints.some(c =>
|
|
118
|
-
c.data.value.columns.every(col => columnIds.includes(col))
|
|
119
|
-
),
|
|
120
|
-
algorithm: idx.algorithm.tag.toLowerCase() as 'btree',
|
|
121
|
-
columns: columnIds.map(getColName),
|
|
122
|
-
};
|
|
123
|
-
}) as T['idxs'],
|
|
163
|
+
// Expose resolved runtime indexes separately so runtime users don't have to
|
|
164
|
+
// reinterpret `indexes` with unsafe casts.
|
|
165
|
+
resolvedIndexes,
|
|
124
166
|
tableDef,
|
|
125
167
|
...(tableDef.isEvent ? { isEvent: true } : {}),
|
|
126
168
|
};
|
package/src/lib/table.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
Indexes,
|
|
18
18
|
IndexOpts,
|
|
19
19
|
ReadonlyIndexes,
|
|
20
|
+
UntypedIndex,
|
|
20
21
|
} from './indexes';
|
|
21
22
|
import ScheduleAt from './schedule_at';
|
|
22
23
|
import type { TableSchema } from './table_schema';
|
|
@@ -116,7 +117,25 @@ export type UntypedTableDef = {
|
|
|
116
117
|
columns: Record<string, ColumnBuilder<any, any, ColumnMetadata<any>>>;
|
|
117
118
|
// This is really just a ProductType where all the elements have names.
|
|
118
119
|
rowType: RowBuilder<RowObj>['algebraicType']['value'];
|
|
120
|
+
/**
|
|
121
|
+
* Declarative multi-column indexes supplied by user code in `table({ indexes: [...] }, ...)`.
|
|
122
|
+
*
|
|
123
|
+
* This is intentionally the *declarative* shape (`IndexOpts`) because a lot of
|
|
124
|
+
* type-level behavior is derived from these entries (for example query-builder
|
|
125
|
+
* inference over composite indexes).
|
|
126
|
+
*/
|
|
119
127
|
indexes: readonly IndexOpts<any>[];
|
|
128
|
+
/**
|
|
129
|
+
* Fully-resolved runtime indexes materialized from `RawTableDefV10`.
|
|
130
|
+
*
|
|
131
|
+
* This contains both:
|
|
132
|
+
* 1) field-level indexes inferred from column metadata, and
|
|
133
|
+
* 2) explicit table-level indexes.
|
|
134
|
+
*
|
|
135
|
+
* Runtime consumers like `TableCacheImpl` should use this field instead of
|
|
136
|
+
* reinterpreting `indexes` as runtime index metadata.
|
|
137
|
+
*/
|
|
138
|
+
resolvedIndexes: readonly UntypedIndex<any>[];
|
|
120
139
|
constraints: readonly ConstraintOpts<any>[];
|
|
121
140
|
tableDef: RawTableDefV10;
|
|
122
141
|
isEvent?: boolean;
|
|
@@ -220,7 +239,12 @@ export type ReadonlyTable<TableDef extends UntypedTableDef> = Prettify<
|
|
|
220
239
|
>;
|
|
221
240
|
|
|
222
241
|
export interface ReadonlyTableMethods<TableDef extends UntypedTableDef> {
|
|
223
|
-
/**
|
|
242
|
+
/**
|
|
243
|
+
* Returns the number of rows in this table.
|
|
244
|
+
*
|
|
245
|
+
* This reads datastore metadata, so it runs in constant time.
|
|
246
|
+
* It also takes into account modifications by the current transaction.
|
|
247
|
+
*/
|
|
224
248
|
count(): bigint;
|
|
225
249
|
|
|
226
250
|
/** Iterate over all rows in the TX state. Rust Iterator<Item=Row> → TS IterableIterator<Row>. */
|
|
@@ -400,6 +424,14 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
|
|
|
400
424
|
|
|
401
425
|
// convert explicit multi‑column indexes coming from options.indexes
|
|
402
426
|
for (const indexOpts of userIndexes ?? []) {
|
|
427
|
+
const accessor = indexOpts.accessor;
|
|
428
|
+
if (typeof accessor !== 'string' || accessor.length === 0) {
|
|
429
|
+
const tableLabel = name ?? '<unnamed>';
|
|
430
|
+
const indexLabel = indexOpts.name ?? '<unnamed>';
|
|
431
|
+
throw new TypeError(
|
|
432
|
+
`Index '${indexLabel}' on table '${tableLabel}' must define a non-empty 'accessor'`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
403
435
|
let algorithm: RawIndexAlgorithm;
|
|
404
436
|
switch (indexOpts.algorithm) {
|
|
405
437
|
case 'btree':
|
|
@@ -418,16 +450,19 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
|
|
|
418
450
|
algorithm = { tag: 'Direct', value: colIds.get(indexOpts.column)! };
|
|
419
451
|
break;
|
|
420
452
|
}
|
|
421
|
-
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
453
|
+
|
|
454
|
+
// Unnamed indexes are assigned a globally unique source name.
|
|
455
|
+
// `accessor` controls the TypeScript property used to access the index.
|
|
456
|
+
// `name` (if present) is preserved as the canonical schema name.
|
|
457
|
+
//
|
|
458
|
+
// IMPORTANT: we intentionally do not reject duplicate accessor names here.
|
|
459
|
+
// This preserves existing behavior for raw table definitions. Downstream
|
|
460
|
+
// runtime consumers decide how duplicates are resolved:
|
|
461
|
+
// - server runtime merges duplicate accessors onto one accessor object
|
|
462
|
+
// - client cache assignment is last-write-wins for duplicate accessors
|
|
428
463
|
indexes.push({
|
|
429
464
|
sourceName: undefined,
|
|
430
|
-
accessorName:
|
|
465
|
+
accessorName: accessor,
|
|
431
466
|
algorithm,
|
|
432
467
|
canonicalName: indexOpts.name,
|
|
433
468
|
});
|
|
@@ -496,7 +531,9 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
|
|
|
496
531
|
isEvent,
|
|
497
532
|
};
|
|
498
533
|
},
|
|
499
|
-
|
|
534
|
+
// Preserve the declared index options as runtime data so `tableToSchema`
|
|
535
|
+
// can expose them without type-smuggling.
|
|
536
|
+
idxs: userIndexes as OptsIndices<Opts>,
|
|
500
537
|
constraints: constraints as OptsConstraints<Opts>,
|
|
501
538
|
schedule,
|
|
502
539
|
};
|
package/src/react/useTable.ts
CHANGED
|
@@ -104,6 +104,8 @@ export function useTable<TableDef extends UntypedTableDef>(
|
|
|
104
104
|
) as Prettify<UseTableRowType>[])
|
|
105
105
|
: (Array.from(table.iter()) as Prettify<UseTableRowType>[]);
|
|
106
106
|
return [result, subscribeApplied];
|
|
107
|
+
// TODO: investigating refactoring so that this is no longer necessary, as we have had genuine bugs with missed deps.
|
|
108
|
+
// See https://github.com/clockworklabs/SpacetimeDB/pull/4580.
|
|
107
109
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
108
110
|
}, [connectionState, accessorName, querySql, subscribeApplied]);
|
|
109
111
|
|
|
@@ -205,11 +207,14 @@ export function useTable<TableDef extends UntypedTableDef>(
|
|
|
205
207
|
table.removeOnUpdate?.(onUpdate);
|
|
206
208
|
};
|
|
207
209
|
},
|
|
210
|
+
// TODO: investigating refactoring so that this is no longer necessary, as we have had genuine bugs with missed deps.
|
|
211
|
+
// See https://github.com/clockworklabs/SpacetimeDB/pull/4580.
|
|
208
212
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
209
213
|
[
|
|
210
214
|
connectionState,
|
|
211
215
|
accessorName,
|
|
212
216
|
querySql,
|
|
217
|
+
computeSnapshot,
|
|
213
218
|
callbacks?.onDelete,
|
|
214
219
|
callbacks?.onInsert,
|
|
215
220
|
callbacks?.onUpdate,
|
|
@@ -37,8 +37,10 @@ import {
|
|
|
37
37
|
type PendingCallback,
|
|
38
38
|
type TableUpdate as CacheTableUpdate,
|
|
39
39
|
} from './table_cache.ts';
|
|
40
|
-
import {
|
|
41
|
-
|
|
40
|
+
import {
|
|
41
|
+
WebsocketDecompressAdapter,
|
|
42
|
+
type WebsocketAdapter,
|
|
43
|
+
} from './websocket_decompress_adapter.ts';
|
|
42
44
|
import {
|
|
43
45
|
SubscriptionBuilderImpl,
|
|
44
46
|
SubscriptionHandleImpl,
|
|
@@ -146,7 +148,7 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
|
|
|
146
148
|
#eventId = 0;
|
|
147
149
|
#emitter: EventEmitter<ConnectionEvent>;
|
|
148
150
|
#messageQueue = Promise.resolve();
|
|
149
|
-
#outboundQueue:
|
|
151
|
+
#outboundQueue: Uint8Array[] = [];
|
|
150
152
|
#subscriptionManager = new SubscriptionManager<RemoteModule>();
|
|
151
153
|
#remoteModule: RemoteModule;
|
|
152
154
|
#reducerCallbacks = new Map<
|
|
@@ -171,10 +173,8 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
|
|
|
171
173
|
// private fields.
|
|
172
174
|
// We use them in testing.
|
|
173
175
|
private clientCache: ClientCache<RemoteModule>;
|
|
174
|
-
private ws?:
|
|
175
|
-
private wsPromise: Promise<
|
|
176
|
-
WebsocketDecompressAdapter | WebsocketTestAdapter | undefined
|
|
177
|
-
>;
|
|
176
|
+
private ws?: WebsocketAdapter;
|
|
177
|
+
private wsPromise: Promise<WebsocketAdapter | undefined>;
|
|
178
178
|
|
|
179
179
|
constructor({
|
|
180
180
|
uri,
|
|
@@ -302,6 +302,8 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
|
|
|
302
302
|
#makeReducers(def: RemoteModule): ReducersView<RemoteModule> {
|
|
303
303
|
const out: Record<string, unknown> = {};
|
|
304
304
|
|
|
305
|
+
const writer = new BinaryWriter(1024);
|
|
306
|
+
|
|
305
307
|
for (const reducer of def.reducers) {
|
|
306
308
|
const reducerName = reducer.name;
|
|
307
309
|
const key = reducer.accessorName;
|
|
@@ -310,7 +312,7 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
|
|
|
310
312
|
this.#reducerArgsSerializers[reducerName];
|
|
311
313
|
|
|
312
314
|
(out as any)[key] = (params: InferTypeOfRow<typeof reducer.params>) => {
|
|
313
|
-
|
|
315
|
+
writer.clear();
|
|
314
316
|
serializeArgs(writer, params);
|
|
315
317
|
const argsBuffer = writer.getBuffer();
|
|
316
318
|
return this.callReducer(reducerName, argsBuffer, params);
|
|
@@ -323,6 +325,8 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
|
|
|
323
325
|
#makeProcedures(def: RemoteModule): ProceduresView<RemoteModule> {
|
|
324
326
|
const out: Record<string, unknown> = {};
|
|
325
327
|
|
|
328
|
+
const writer = new BinaryWriter(1024);
|
|
329
|
+
|
|
326
330
|
for (const procedure of def.procedures) {
|
|
327
331
|
const procedureName = procedure.name;
|
|
328
332
|
const key = procedure.accessorName;
|
|
@@ -333,7 +337,7 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
|
|
|
333
337
|
(out as any)[key] = (
|
|
334
338
|
params: InferTypeOfRow<typeof procedure.params>
|
|
335
339
|
): Promise<any> => {
|
|
336
|
-
|
|
340
|
+
writer.clear();
|
|
337
341
|
serializeArgs(writer, params);
|
|
338
342
|
const argsBuffer = writer.getBuffer();
|
|
339
343
|
return this.callProcedure(procedureName, argsBuffer).then(returnBuf => {
|
|
@@ -537,41 +541,36 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
|
|
|
537
541
|
return this.#mergeTableUpdates(updates);
|
|
538
542
|
}
|
|
539
543
|
|
|
540
|
-
#
|
|
541
|
-
wsResolved: WebsocketDecompressAdapter | WebsocketTestAdapter,
|
|
542
|
-
message: ClientMessage
|
|
543
|
-
): void {
|
|
544
|
-
stdbLogger(
|
|
545
|
-
'trace',
|
|
546
|
-
() => `Sending message to server: ${stringify(message)}`
|
|
547
|
-
);
|
|
548
|
-
const writer = new BinaryWriter(1024);
|
|
549
|
-
ClientMessage.serialize(writer, message);
|
|
550
|
-
const encoded = writer.getBuffer();
|
|
551
|
-
wsResolved.send(encoded);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
#flushOutboundQueue(
|
|
555
|
-
wsResolved: WebsocketDecompressAdapter | WebsocketTestAdapter
|
|
556
|
-
): void {
|
|
557
|
-
if (!this.isActive || this.#outboundQueue.length === 0) {
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
544
|
+
#flushOutboundQueue(wsResolved: WebsocketAdapter): void {
|
|
560
545
|
const pending = this.#outboundQueue.splice(0);
|
|
561
546
|
for (const message of pending) {
|
|
562
|
-
|
|
547
|
+
wsResolved.send(message);
|
|
563
548
|
}
|
|
564
549
|
}
|
|
565
550
|
|
|
551
|
+
#clientMessageEncoder = new BinaryWriter(1024);
|
|
566
552
|
#sendMessage(message: ClientMessage): void {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
this.#
|
|
574
|
-
|
|
553
|
+
const writer = this.#clientMessageEncoder;
|
|
554
|
+
writer.clear();
|
|
555
|
+
ClientMessage.serialize(writer, message);
|
|
556
|
+
const encoded = writer.getBuffer();
|
|
557
|
+
|
|
558
|
+
if (this.ws && this.isActive) {
|
|
559
|
+
if (this.#outboundQueue.length) this.#flushOutboundQueue(this.ws);
|
|
560
|
+
|
|
561
|
+
stdbLogger(
|
|
562
|
+
'trace',
|
|
563
|
+
() => `Sending message to server: ${stringify(message)}`
|
|
564
|
+
);
|
|
565
|
+
this.ws.send(encoded);
|
|
566
|
+
} else {
|
|
567
|
+
stdbLogger(
|
|
568
|
+
'trace',
|
|
569
|
+
() => `Queuing message to server: ${stringify(message)}`
|
|
570
|
+
);
|
|
571
|
+
// use slice() to copy, in case the clientMessageEncoder's buffer gets used
|
|
572
|
+
this.#outboundQueue.push(encoded.slice());
|
|
573
|
+
}
|
|
575
574
|
}
|
|
576
575
|
|
|
577
576
|
#nextEventId(): string {
|
|
@@ -978,11 +977,7 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
|
|
|
978
977
|
* ```
|
|
979
978
|
*/
|
|
980
979
|
disconnect(): void {
|
|
981
|
-
this.wsPromise.then(
|
|
982
|
-
if (wsResolved) {
|
|
983
|
-
wsResolved.close();
|
|
984
|
-
}
|
|
985
|
-
});
|
|
980
|
+
this.wsPromise.then(ws => ws?.close());
|
|
986
981
|
}
|
|
987
982
|
|
|
988
983
|
private on(
|