spacetimedb 2.0.3 → 2.0.4
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 +3 -0
- package/dist/angular/index.cjs.map +1 -1
- package/dist/angular/index.mjs +3 -0
- package/dist/angular/index.mjs.map +1 -1
- package/dist/browser/angular/index.mjs +3 -0
- package/dist/browser/angular/index.mjs.map +1 -1
- package/dist/browser/react/index.mjs +3 -0
- package/dist/browser/react/index.mjs.map +1 -1
- package/dist/browser/svelte/index.mjs +3 -0
- package/dist/browser/svelte/index.mjs.map +1 -1
- package/dist/browser/vue/index.mjs +3 -0
- package/dist/browser/vue/index.mjs.map +1 -1
- package/dist/index.browser.mjs +126 -92
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.cjs +126 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +126 -92
- package/dist/index.mjs.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 +4 -2
- 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 +19 -1
- 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 +3 -0
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.mjs +3 -0
- package/dist/react/index.mjs.map +1 -1
- package/dist/sdk/db_connection_impl.d.ts.map +1 -1
- package/dist/sdk/index.browser.mjs +126 -92
- package/dist/sdk/index.browser.mjs.map +1 -1
- package/dist/sdk/index.cjs +126 -92
- package/dist/sdk/index.cjs.map +1 -1
- package/dist/sdk/index.mjs +126 -92
- 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.mjs +65 -21
- package/dist/server/index.mjs.map +1 -1
- package/dist/svelte/index.cjs +3 -0
- package/dist/svelte/index.cjs.map +1 -1
- package/dist/svelte/index.mjs +3 -0
- 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 +24 -0
- package/dist/tanstack/index.cjs.map +1 -1
- package/dist/tanstack/index.mjs +24 -0
- package/dist/tanstack/index.mjs.map +1 -1
- package/dist/vue/index.cjs +3 -0
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.mjs +3 -0
- package/dist/vue/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/lib/binary_writer.ts +4 -0
- package/src/lib/indexes.ts +1 -1
- package/src/lib/query.ts +30 -6
- package/src/lib/schema.ts +66 -24
- package/src/lib/table.ts +41 -9
- 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/runtime.ts +7 -3
- package/src/server/schema.test-d.ts +37 -0
- package/src/server/view.test-d.ts +2 -0
- package/src/tanstack/SpacetimeDBQueryClient.ts +24 -0
package/package.json
CHANGED
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
|
}
|
|
@@ -628,6 +629,11 @@ export type ValueExpr<TableDef extends TypedTableDef, Value> =
|
|
|
628
629
|
| LiteralExpr<Value & LiteralValue>
|
|
629
630
|
| ColumnExprForValue<TableDef, Value>;
|
|
630
631
|
|
|
632
|
+
type PredicateExpr<TableDef extends TypedTableDef> =
|
|
633
|
+
| BooleanExpr<TableDef>
|
|
634
|
+
| ColumnExprForValue<TableDef, SatsBool>
|
|
635
|
+
| boolean;
|
|
636
|
+
|
|
631
637
|
type LiteralExpr<Value> = {
|
|
632
638
|
type: 'literal';
|
|
633
639
|
value: Value;
|
|
@@ -654,6 +660,24 @@ function normalizeValue(val: ValueInput<any>): ValueExpr<any, any> {
|
|
|
654
660
|
return literal(val as LiteralValue);
|
|
655
661
|
}
|
|
656
662
|
|
|
663
|
+
function normalizePredicateExpr<TableDef extends TypedTableDef>(
|
|
664
|
+
value: PredicateExpr<TableDef>
|
|
665
|
+
): BooleanExpr<TableDef> {
|
|
666
|
+
if (value instanceof BooleanExpr) return value;
|
|
667
|
+
if (typeof value === 'boolean') {
|
|
668
|
+
return new BooleanExpr({
|
|
669
|
+
type: 'eq',
|
|
670
|
+
left: literal(value),
|
|
671
|
+
right: literal(true),
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
return new BooleanExpr({
|
|
675
|
+
type: 'eq',
|
|
676
|
+
left: value as ValueExpr<TableDef, any>,
|
|
677
|
+
right: literal(true),
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
657
681
|
type EqExpr<Table extends TypedTableDef = any> = BooleanExpr<Table>;
|
|
658
682
|
|
|
659
683
|
type BooleanExprData<Table extends TypedTableDef> = (
|
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;
|
|
@@ -400,6 +419,14 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
|
|
|
400
419
|
|
|
401
420
|
// convert explicit multi‑column indexes coming from options.indexes
|
|
402
421
|
for (const indexOpts of userIndexes ?? []) {
|
|
422
|
+
const accessor = indexOpts.accessor;
|
|
423
|
+
if (typeof accessor !== 'string' || accessor.length === 0) {
|
|
424
|
+
const tableLabel = name ?? '<unnamed>';
|
|
425
|
+
const indexLabel = indexOpts.name ?? '<unnamed>';
|
|
426
|
+
throw new TypeError(
|
|
427
|
+
`Index '${indexLabel}' on table '${tableLabel}' must define a non-empty 'accessor'`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
403
430
|
let algorithm: RawIndexAlgorithm;
|
|
404
431
|
switch (indexOpts.algorithm) {
|
|
405
432
|
case 'btree':
|
|
@@ -418,16 +445,19 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
|
|
|
418
445
|
algorithm = { tag: 'Direct', value: colIds.get(indexOpts.column)! };
|
|
419
446
|
break;
|
|
420
447
|
}
|
|
421
|
-
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
448
|
+
|
|
449
|
+
// Unnamed indexes are assigned a globally unique source name.
|
|
450
|
+
// `accessor` controls the TypeScript property used to access the index.
|
|
451
|
+
// `name` (if present) is preserved as the canonical schema name.
|
|
452
|
+
//
|
|
453
|
+
// IMPORTANT: we intentionally do not reject duplicate accessor names here.
|
|
454
|
+
// This preserves existing behavior for raw table definitions. Downstream
|
|
455
|
+
// runtime consumers decide how duplicates are resolved:
|
|
456
|
+
// - server runtime merges duplicate accessors onto one accessor object
|
|
457
|
+
// - client cache assignment is last-write-wins for duplicate accessors
|
|
428
458
|
indexes.push({
|
|
429
459
|
sourceName: undefined,
|
|
430
|
-
accessorName:
|
|
460
|
+
accessorName: accessor,
|
|
431
461
|
algorithm,
|
|
432
462
|
canonicalName: indexOpts.name,
|
|
433
463
|
});
|
|
@@ -496,7 +526,9 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
|
|
|
496
526
|
isEvent,
|
|
497
527
|
};
|
|
498
528
|
},
|
|
499
|
-
|
|
529
|
+
// Preserve the declared index options as runtime data so `tableToSchema`
|
|
530
|
+
// can expose them without type-smuggling.
|
|
531
|
+
idxs: userIndexes as OptsIndices<Opts>,
|
|
500
532
|
constraints: constraints as OptsConstraints<Opts>,
|
|
501
533
|
schedule,
|
|
502
534
|
};
|
|
@@ -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(
|
package/src/sdk/table_cache.ts
CHANGED
|
@@ -12,7 +12,6 @@ import type {
|
|
|
12
12
|
ReadonlyIndexes,
|
|
13
13
|
ReadonlyRangedIndex,
|
|
14
14
|
ReadonlyUniqueIndex,
|
|
15
|
-
UntypedIndex,
|
|
16
15
|
} from '../lib/indexes.ts';
|
|
17
16
|
import type { Bound } from '../server/range.ts';
|
|
18
17
|
import type { Prettify } from '../lib/type_util.ts';
|
|
@@ -84,23 +83,27 @@ export class TableCacheImpl<
|
|
|
84
83
|
this.tableDef = tableDef;
|
|
85
84
|
this.rows = new Map();
|
|
86
85
|
this.emitter = new EventEmitter();
|
|
87
|
-
// Build
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
// Build index views from the resolved runtime index metadata.
|
|
87
|
+
//
|
|
88
|
+
// We intentionally use `resolvedIndexes` rather than `indexes`:
|
|
89
|
+
// - `indexes` is declarative table-level config (`IndexOpts`) used mainly for typing.
|
|
90
|
+
// - `resolvedIndexes` is the runtime shape (`UntypedIndex`) that includes both
|
|
91
|
+
// field-level and explicit table-level indexes.
|
|
92
|
+
for (const idxDef of this.tableDef.resolvedIndexes) {
|
|
94
93
|
const index = this.#makeReadonlyIndex(this.tableDef, idxDef);
|
|
94
|
+
// IMPORTANT: for duplicate accessor names, client cache uses assignment
|
|
95
|
+
// semantics and later entries overwrite earlier ones. This matches prior
|
|
96
|
+
// behavior and is intentionally different from server runtime merge logic.
|
|
95
97
|
(this as any)[idxDef.name] = index;
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
// TODO: this just scans the whole table; we should build proper index structures
|
|
100
102
|
#makeReadonlyIndex<
|
|
101
|
-
I extends
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
I extends TableDefForTableName<
|
|
104
|
+
RemoteModule,
|
|
105
|
+
TableName
|
|
106
|
+
>['resolvedIndexes'][number],
|
|
104
107
|
>(
|
|
105
108
|
tableDef: TableDefForTableName<RemoteModule, TableName>,
|
|
106
109
|
idx: I
|
|
@@ -1,48 +1,55 @@
|
|
|
1
1
|
import { decompress } from './decompress';
|
|
2
2
|
import { resolveWS } from './ws';
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const buffer = new Uint8Array(msg.data);
|
|
14
|
-
let decompressed: Uint8Array;
|
|
15
|
-
|
|
16
|
-
if (buffer[0] === 0) {
|
|
17
|
-
decompressed = buffer.slice(1);
|
|
18
|
-
} else if (buffer[0] === 1) {
|
|
19
|
-
throw new Error(
|
|
20
|
-
'Brotli Compression not supported. Please use gzip or none compression in withCompression method on DbConnection.'
|
|
21
|
-
);
|
|
22
|
-
} else if (buffer[0] === 2) {
|
|
23
|
-
decompressed = await decompress(buffer.slice(1), 'gzip');
|
|
24
|
-
} else {
|
|
25
|
-
throw new Error(
|
|
26
|
-
'Unexpected Compression Algorithm. Please use `gzip` or `none`'
|
|
27
|
-
);
|
|
28
|
-
}
|
|
4
|
+
export interface WebsocketAdapter {
|
|
5
|
+
send(msg: Uint8Array): void;
|
|
6
|
+
close(): void;
|
|
7
|
+
|
|
8
|
+
set onclose(handler: (ev: CloseEvent) => void);
|
|
9
|
+
set onopen(handler: () => void);
|
|
10
|
+
set onmessage(handler: (msg: { data: Uint8Array }) => void);
|
|
11
|
+
set onerror(handler: (msg: ErrorEvent) => void);
|
|
12
|
+
}
|
|
29
13
|
|
|
30
|
-
|
|
14
|
+
export class WebsocketDecompressAdapter implements WebsocketAdapter {
|
|
15
|
+
set onclose(handler: (ev: CloseEvent) => void) {
|
|
16
|
+
this.#ws.onclose = handler;
|
|
31
17
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.onopen?.(msg);
|
|
18
|
+
set onopen(handler: () => void) {
|
|
19
|
+
this.#ws.onopen = handler;
|
|
35
20
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
set onmessage(handler: (msg: { data: Uint8Array }) => void) {
|
|
22
|
+
this.#ws.onmessage = async (msg: MessageEvent<ArrayBuffer>) => {
|
|
23
|
+
const data = await this.#decompress(new Uint8Array(msg.data));
|
|
24
|
+
handler({ data });
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
set onerror(handler: (msg: ErrorEvent) => void) {
|
|
28
|
+
this.#ws.onerror = handler as (msg: Event) => void;
|
|
39
29
|
}
|
|
40
30
|
|
|
41
|
-
#
|
|
42
|
-
|
|
31
|
+
#ws: WebSocket;
|
|
32
|
+
|
|
33
|
+
async #decompress(buffer: Uint8Array): Promise<Uint8Array> {
|
|
34
|
+
const tag = buffer[0];
|
|
35
|
+
const data = buffer.subarray(1);
|
|
36
|
+
switch (tag) {
|
|
37
|
+
case 0:
|
|
38
|
+
return data;
|
|
39
|
+
case 1:
|
|
40
|
+
throw new Error(
|
|
41
|
+
'Brotli Compression not supported. Please use gzip or none compression in withCompression method on DbConnection.'
|
|
42
|
+
);
|
|
43
|
+
case 2:
|
|
44
|
+
return await decompress(data, 'gzip');
|
|
45
|
+
default:
|
|
46
|
+
throw new Error(
|
|
47
|
+
'Unexpected Compression Algorithm. Please use `gzip` or `none`'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
43
50
|
}
|
|
44
51
|
|
|
45
|
-
send(msg:
|
|
52
|
+
send(msg: Uint8Array): void {
|
|
46
53
|
this.#ws.send(msg);
|
|
47
54
|
}
|
|
48
55
|
|
|
@@ -51,16 +58,6 @@ export class WebsocketDecompressAdapter {
|
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
constructor(ws: WebSocket) {
|
|
54
|
-
this.onmessage = undefined;
|
|
55
|
-
this.onopen = undefined;
|
|
56
|
-
this.onmessage = undefined;
|
|
57
|
-
this.onerror = undefined;
|
|
58
|
-
|
|
59
|
-
ws.onmessage = this.#handleOnMessage.bind(this);
|
|
60
|
-
ws.onerror = this.#handleOnError.bind(this);
|
|
61
|
-
ws.onclose = this.#handleOnClose.bind(this);
|
|
62
|
-
ws.onopen = this.#handleOnOpen.bind(this);
|
|
63
|
-
|
|
64
61
|
ws.binaryType = 'arraybuffer';
|
|
65
62
|
|
|
66
63
|
this.#ws = ws;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { BinaryReader, BinaryWriter } from '../';
|
|
2
2
|
import { ClientMessage, ServerMessage } from './client_api/types';
|
|
3
|
+
import type { WebsocketAdapter } from './websocket_decompress_adapter';
|
|
3
4
|
|
|
4
|
-
class WebsocketTestAdapter {
|
|
5
|
+
class WebsocketTestAdapter implements WebsocketAdapter {
|
|
5
6
|
onclose: any;
|
|
6
7
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
7
|
-
onopen!:
|
|
8
|
+
onopen!: () => void;
|
|
8
9
|
onmessage: any;
|
|
9
10
|
onerror: any;
|
|
10
11
|
|
package/src/server/runtime.ts
CHANGED
|
@@ -516,6 +516,7 @@ function makeTableView(
|
|
|
516
516
|
) as Table<any>;
|
|
517
517
|
|
|
518
518
|
for (const indexDef of table.indexes) {
|
|
519
|
+
const accessorName = indexDef.accessorName!;
|
|
519
520
|
const index_id = sys.index_id_from_name(indexDef.sourceName!);
|
|
520
521
|
|
|
521
522
|
let column_ids: number[];
|
|
@@ -795,10 +796,13 @@ function makeTableView(
|
|
|
795
796
|
} as RangedIndex<any, any>;
|
|
796
797
|
}
|
|
797
798
|
|
|
798
|
-
|
|
799
|
-
|
|
799
|
+
// IMPORTANT: duplicate accessor handling.
|
|
800
|
+
// When multiple raw indexes share the same accessor name, we merge index
|
|
801
|
+
// methods onto a single accessor object instead of throwing.
|
|
802
|
+
if (Object.hasOwn(tableView, accessorName)) {
|
|
803
|
+
freeze(Object.assign((tableView as any)[accessorName], index));
|
|
800
804
|
} else {
|
|
801
|
-
tableView[
|
|
805
|
+
(tableView as any)[accessorName] = freeze(index);
|
|
802
806
|
}
|
|
803
807
|
}
|
|
804
808
|
|