Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions crates/bindings-typescript/src/sdk/db_connection_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ import type {
} from './message_types.ts';
import type { ReducerEvent } from './reducer_event.ts';
import { type UntypedRemoteModule } from './spacetime_module.ts';
import { makeQueryBuilder } from '../lib/query';
import {
makeFromBuilder,
makeQueryBuilder,
type SubscriptionFromBuilder,
} from '../lib/query';
import {
type TableCache,
type Operation,
Expand All @@ -52,6 +56,7 @@ import type {
} from './reducers.ts';
import type { ClientDbView } from './db_view.ts';
import type { RowType, UntypedTableDef } from '../lib/table.ts';
import type { UntypedSchemaDef } from '../lib/schema';
import type { ProceduresView } from './procedures.ts';
import type { Values } from '../lib/type_util.ts';
import type { TransactionUpdate } from './client_api/types.ts';
Expand Down Expand Up @@ -457,6 +462,10 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
return makeQueryBuilder({ tables: this.#remoteModule.tables } as any);
}

getFromBuilder<SchemaDef extends UntypedSchemaDef>(): SubscriptionFromBuilder<SchemaDef> {
return makeFromBuilder<SchemaDef>(this.#remoteModule.tables as SchemaDef['tables']);
}

registerSubscription(
handle: SubscriptionHandleImpl<RemoteModule>,
handleEmitter: EventEmitter<
Expand Down Expand Up @@ -503,8 +512,10 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
const rows: Operation[] = [];

const deserializeRow = this.#rowDeserializers[tableName];
const { primaryKeyColName, primaryKeyColType } =
this.#rowIdMetadata[tableName];
if (!deserializeRow) return [];
const rowIdInfo = this.#rowIdMetadata[tableName];
if (!rowIdInfo) return [];
const { primaryKeyColName, primaryKeyColType } = rowIdInfo;
let previousOffset = 0;
while (reader.remaining > 0) {
const row = deserializeRow(reader);
Expand Down Expand Up @@ -793,6 +804,7 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
// Get table information for the table being updated
const tableName = tableUpdate.tableName;
const tableDef = this.#sourceNameToTableDef[tableName];
if (!tableDef) continue;
const table = this.clientCache.getOrCreateTable(tableDef);
const newCallbacks = table.applyOperations(
tableUpdate.operations as Operation<
Expand Down
48 changes: 45 additions & 3 deletions crates/bindings-typescript/src/sdk/subscription_builder_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@ import type {
} from './event_context';
import { EventEmitter } from './event_emitter';
import type { UntypedRemoteModule } from './spacetime_module';
import { isRowTypedQuery, toSql, type RowTypedQuery } from '../lib/query';
import {
isRowTypedQuery,
toSql,
type SubscriptionFromBuilder,
type RowTypedQuery,
} from '../lib/query';
import type { UntypedSchemaDef } from '../lib/schema';
import type { Values } from '../lib/type_util';

export class SubscriptionBuilderImpl<RemoteModule extends UntypedRemoteModule> {
#onApplied?: (ctx: SubscriptionEventContextInterface<RemoteModule>) => void =
undefined;
#onError?: (ctx: ErrorContextInterface<RemoteModule>) => void = undefined;
#pendingQueries: Array<string | RowTypedQuery<any, any>> = [];
constructor(private db: DbConnectionImpl<RemoteModule>) {}

/**
Expand Down Expand Up @@ -64,6 +71,32 @@ export class SubscriptionBuilderImpl<RemoteModule extends UntypedRemoteModule> {
return this;
}

/**
* Accumulates a query for a later `subscribe()` call.
* Queries added via `addQuery` and queries passed directly to `subscribe` are mutually exclusive —
* call `subscribe()` with no arguments to send all accumulated queries.
*
* @param queryFn - Receives `{ from }`, where `from` exposes all tables (root and namespaced).
* @returns The current `SubscriptionBuilder` instance for chaining.
*
* @example
* ```ts
* conn.subscriptionBuilder()
* .addQuery(q => q.from.players.build())
* .addQuery(q => q.from.inventory.items.build())
* .subscribe();
* ```
*/
addQuery(
queryFn: (q: {
from: SubscriptionFromBuilder<RemoteModule & UntypedSchemaDef>;
}) => RowTypedQuery<any, any>
): this {
const from = this.db.getFromBuilder<RemoteModule & UntypedSchemaDef>();
this.#pendingQueries.push(queryFn({ from }));
return this;
}

/**
* Subscribe to a single query. The results of the query will be merged into the client
* cache and deduplicated on the client.
Expand All @@ -80,6 +113,7 @@ export class SubscriptionBuilderImpl<RemoteModule extends UntypedRemoteModule> {
* subscription.unsubscribe();
* ```
*/
subscribe(): SubscriptionHandleImpl<RemoteModule>;
subscribe(
query_sql: string | RowTypedQuery<any, any>
): SubscriptionHandleImpl<RemoteModule>;
Expand All @@ -92,14 +126,22 @@ export class SubscriptionBuilderImpl<RemoteModule extends UntypedRemoteModule> {
) => RowTypedQuery<any, any> | RowTypedQuery<any, any>[]
): SubscriptionHandleImpl<RemoteModule>;
subscribe(
query_sql:
query_sql?:
| string
| RowTypedQuery<any, any>
| Array<string | RowTypedQuery<any, any>>
| ((tables: any) => RowTypedQuery<any, any> | RowTypedQuery<any, any>[])
): SubscriptionHandleImpl<RemoteModule> {
let queries: Array<string | RowTypedQuery<any, any>>;
if (typeof query_sql === 'function') {
if (query_sql === undefined) {
if (this.#pendingQueries.length === 0) {
throw new Error(
'subscriptionBuilder().subscribe() called with no queries; use addQuery() first or pass a query argument'
);
}
queries = this.#pendingQueries;
this.#pendingQueries = [];
} else if (typeof query_sql === 'function') {
const tablesMap = this.db.getTablesMap?.();
const result = query_sql(tablesMap);
queries = Array.isArray(result) ? result : [result];
Expand Down
69 changes: 69 additions & 0 deletions crates/codegen/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use spacetimedb_lib::db::raw_def::v9::TableAccess;
use spacetimedb_schema::def::{ModuleDef, ProcedureDef, ReducerDef, TableDef, TypeDef, ViewDef};
use spacetimedb_schema::schema::{Schema, TableSchema};
mod code_indenter;
Expand Down Expand Up @@ -33,10 +34,34 @@ pub fn generate(module: &ModuleDef, lang: &dyn Lang, options: &CodegenOptions) -
itertools::chain!(
util::iter_tables(module, options.visibility).map(|tbl| lang.generate_table_file(module, tbl)),
module.views().map(|view| lang.generate_view_file(module, view)),
// Public tables from mounted submodules
module
.all_tables_with_prefix()
.into_iter()
.filter(|(prefix, _, table)| !prefix.is_empty() && table.table_access == TableAccess::Public)
.map(|(prefix, owning_def, table)| lang.generate_mounted_table_file(owning_def, &prefix, table)),
// Views from mounted submodules (views are currently always public)
module
.all_views_with_prefix()
.into_iter()
.filter(|(prefix, _, _)| !prefix.is_empty())
.map(|(prefix, owning_def, view)| lang.generate_mounted_view_file(owning_def, &prefix, view)),
module.types().flat_map(|typ| lang.generate_type_files(module, typ)),
util::iter_reducers(module, options.visibility).map(|reducer| lang.generate_reducer_file(module, reducer)),
util::iter_procedures(module, options.visibility)
.map(|procedure| lang.generate_procedure_file(module, procedure)),
// Reducers from mounted submodules
module
.all_reducers_with_prefix()
.into_iter()
.filter(|(prefix, _, reducer)| !prefix.is_empty() && !reducer.visibility.is_private())
.map(|(prefix, owning_def, reducer)| lang.generate_mounted_reducer_file(owning_def, &prefix, reducer)),
// Procedures from mounted submodules
module
.all_procedures_with_prefix()
.into_iter()
.filter(|(prefix, _, procedure)| !prefix.is_empty() && !procedure.visibility.is_private())
.map(|(prefix, owning_def, procedure)| lang.generate_mounted_procedure_file(owning_def, &prefix, procedure)),
lang.generate_global_files(module, options),
)
.collect()
Expand Down Expand Up @@ -68,4 +93,48 @@ pub trait Lang {
.expect("Failed to generate table due to validation errors");
self.generate_table_file_from_schema(module, &tbl, schema)
}

/// Generate a row-type file for a public table from a mounted submodule.
/// Uses `owning_def`'s typespace for type resolution.
/// Filename goes in a subdirectory named after the namespace:
/// e.g. `alias/table_name_table.ts` for namespace `"alias."`, table `tableName`.
fn generate_mounted_table_file(&self, owning_def: &ModuleDef, namespace: &str, table: &TableDef) -> OutputFile {
let schema = TableSchema::from_module_def(owning_def, table, (), 0.into())
.validated()
.expect("Failed to generate mounted table file");
let mut file = self.generate_table_file_from_schema(owning_def, table, schema);
let ns_path = namespace.trim_end_matches('.').replace('.', "/");
file.filename = format!("{}/{}", ns_path, file.filename);
file
}

/// Generate a row-type file for a view from a mounted submodule.
fn generate_mounted_view_file(&self, owning_def: &ModuleDef, namespace: &str, view: &ViewDef) -> OutputFile {
let tbl = TableDef::from(view.clone());
let schema = TableSchema::from_view_def_for_codegen(owning_def, view)
.validated()
.expect("Failed to generate mounted view file");
let mut file = self.generate_table_file_from_schema(owning_def, &tbl, schema);
let ns_path = namespace.trim_end_matches('.').replace('.', "/");
file.filename = format!("{}/{}", ns_path, file.filename);
file
}

/// Generate an arg-schema file for a reducer from a mounted submodule.
/// Filename goes in a subdirectory named after the namespace prefix:
/// e.g. `lib/library_reducer_reducer.ts` for prefix `"lib/"`.
fn generate_mounted_reducer_file(&self, owning_def: &ModuleDef, prefix: &str, reducer: &ReducerDef) -> OutputFile {
let mut file = self.generate_reducer_file(owning_def, reducer);
let ns_path = prefix.trim_end_matches('/').replace('/', "/");
file.filename = format!("{}/{}", ns_path, file.filename);
file
}

/// Generate an arg-schema file for a procedure from a mounted submodule.
fn generate_mounted_procedure_file(&self, owning_def: &ModuleDef, prefix: &str, procedure: &ProcedureDef) -> OutputFile {
let mut file = self.generate_procedure_file(owning_def, procedure);
let ns_path = prefix.trim_end_matches('/').replace('/', "/");
file.filename = format!("{}/{}", ns_path, file.filename);
file
}
}
Loading
Loading