Skip to content
Merged
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
19 changes: 19 additions & 0 deletions crates/bindings-typescript/src/angular/connection_state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { InjectionToken, type WritableSignal } from '@angular/core';
import type { ConnectionId } from '../lib/connection_id';
import type { Identity } from '../lib/identity';
import type { DbConnectionImpl } from '../sdk/db_connection_impl';

export interface ConnectionState {
isActive: boolean;
identity?: Identity;
token?: string;
connectionId: ConnectionId;
connectionError?: Error;
getConnection<
DbConnection extends DbConnectionImpl<any>,
>(): DbConnection | null;
}

export const SPACETIMEDB_CONNECTION = new InjectionToken<
WritableSignal<ConnectionState>
>('SpacetimeDB Connection State');
17 changes: 4 additions & 13 deletions crates/bindings-typescript/src/angular/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
export * from './injectors';
export * from './providers';
export {
type Value,
type Expr,
eq,
and,
or,
isEq,
isAnd,
isOr,
where,
} from '../lib/filter';
export type { ConnectionState } from './connection_state.ts';
export * from './injectors/index.ts';
export * from './providers/index.ts';
export { eq, where } from '../lib/filter.ts';
8 changes: 4 additions & 4 deletions crates/bindings-typescript/src/angular/injectors/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { injectSpacetimeDB } from './inject-spacetimedb';
export { injectTable, type TableRows } from './inject-table';
export { injectSpacetimeDBConnected } from './inject-spacetimedb-connected';
export { injectReducer } from './inject-reducer';
export { injectSpacetimeDB } from './inject-spacetimedb.ts';
export { injectTable, type TableRows } from './inject-table.ts';
export { injectSpacetimeDBConnected } from './inject-spacetimedb-connected.ts';
export { injectReducer } from './inject-reducer.ts';
42 changes: 27 additions & 15 deletions crates/bindings-typescript/src/angular/injectors/inject-reducer.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import { assertInInjectionContext, inject, effect } from '@angular/core';
import { SPACETIMEDB_CONNECTION } from '../connection_state';
import type { ParamsType } from '../../sdk';
import type { UntypedReducerDef } from '../../sdk/reducers';
import { injectSpacetimeDB } from './inject-spacetimedb';
import { injectSpacetimeDBConnected } from './inject-spacetimedb-connected';
import { DestroyRef, effect, inject } from '@angular/core';

export function injectReducer<ReducerDef extends UntypedReducerDef>(
reducerDef: ReducerDef
) {
const conn = injectSpacetimeDB();
const isActive = injectSpacetimeDBConnected();
const destroyRef = inject(DestroyRef);
): (...params: ParamsType<ReducerDef>) => void {
assertInInjectionContext(injectReducer);

const connState = inject(SPACETIMEDB_CONNECTION);
const queue: ParamsType<ReducerDef>[] = [];
const reducerName = reducerDef.accessorName;

effect(() => {
if (!isActive()) {
// flush queued calls when connection becomes active
effect(onCleanup => {
const state = connState();
if (!state.isActive) {
return;
}

const callReducer = (conn.reducers as any)[reducerName] as (
const connection = state.getConnection();
if (!connection) {
return;
}

const callReducer = (connection.reducers as any)[reducerName] as (
...p: ParamsType<ReducerDef>
) => void;

Expand All @@ -29,19 +34,26 @@ export function injectReducer<ReducerDef extends UntypedReducerDef>(
callReducer(...params);
}
}
});

destroyRef.onDestroy(() => {
queue.splice(0);
onCleanup(() => {
queue.splice(0);
});
});

return (...params: ParamsType<ReducerDef>) => {
if (!isActive()) {
const state = connState();
if (!state.isActive) {
queue.push(params);
return;
}

const connection = state.getConnection();
if (!connection) {
queue.push(params);
return;
}

const callReducer = (conn.reducers as any)[reducerName] as (
const callReducer = (connection.reducers as any)[reducerName] as (
...p: ParamsType<ReducerDef>
) => void;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import { effect, signal, type Signal } from '@angular/core';
import { injectSpacetimeDB } from './inject-spacetimedb';
import {
assertInInjectionContext,
inject,
computed,
type Signal,
} from '@angular/core';
import { SPACETIMEDB_CONNECTION } from '../connection_state';

export function injectSpacetimeDBConnected(): Signal<boolean> {
const conn = injectSpacetimeDB();

const connectedSignal = signal<boolean>(conn.isActive);

// FIXME: Bit of a dirty hack for now, we need to change injectSpacetimeDB
// to return a signal so we can react to changes in connection state properly.
effect(onCleanup => {
const interval = setInterval(() => {
connectedSignal.set(conn.isActive);
}, 100);

onCleanup(() => {
clearInterval(interval);
});
});

return connectedSignal.asReadonly();
assertInInjectionContext(injectSpacetimeDBConnected);
const state = inject(SPACETIMEDB_CONNECTION);
return computed(() => state().isActive);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { assertInInjectionContext, inject } from '@angular/core';
import type { DbConnectionImpl } from '../../sdk';
import { SPACETIMEDB_TOKEN } from '../token';
import { assertInInjectionContext, inject, type Signal } from '@angular/core';
import {
SPACETIMEDB_CONNECTION,
type ConnectionState,
} from '../connection_state';

export function injectSpacetimeDB<T extends DbConnectionImpl<any>>(): T {
export function injectSpacetimeDB(): Signal<ConnectionState> {
assertInInjectionContext(injectSpacetimeDB);
const spacetimedb = inject(SPACETIMEDB_TOKEN);
return spacetimedb as T;
return inject(SPACETIMEDB_CONNECTION).asReadonly();
}
59 changes: 40 additions & 19 deletions crates/bindings-typescript/src/angular/injectors/inject-table.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,41 @@
import { type Signal, signal, effect } from '@angular/core';
import {
assertInInjectionContext,
inject,
signal,
effect,
type Signal,
} from '@angular/core';
import type { RowType, UntypedTableDef } from '../../lib/table';
import type { Prettify } from '../../lib/type_util';
import { injectSpacetimeDB } from './inject-spacetimedb';
import { SPACETIMEDB_CONNECTION } from '../connection_state';
import {
type Value,
type Expr,
type ColumnsFromRow,
eq,
and,
or,
isEq,
isAnd,
isOr,
evaluate,
toString,
where,
classifyMembership,
} from '../../lib/filter';
import type { EventContextInterface } from '../../sdk';
import type { UntypedRemoteModule } from '../../sdk/spacetime_module';

export { eq, and, or, isEq, isAnd, isOr, where };
export type { Value, Expr };

export type RowTypeDef<TableDef extends UntypedTableDef> = Prettify<
RowType<TableDef>
>;

export interface TableRows<TableDef extends UntypedTableDef> {
rows: readonly RowTypeDef<TableDef>[];
isLoading: boolean;
error?: Error;
}

export interface InjectTableCallbacks<RowType> {
Expand All @@ -36,9 +52,7 @@ export interface InjectTableOptions<TableDef extends UntypedTableDef> {
/**
* Angular injection function to subscribe to a table in SpacetimeDB and receive live updates.
*
* This function returns a signal containing the table's rows, filtered by an optional `where` clause,
* and provides a loading state until the initial subscription is applied. It also allows you to specify
* callbacks for row insertions, deletions, and updates.
* Must be called within an injection context (component field initializer or constructor).
*
* @template TableDef The table definition type.
*
Expand Down Expand Up @@ -68,9 +82,11 @@ export function injectTable<TableDef extends UntypedTableDef>(
tableDef: TableDef,
options?: InjectTableOptions<TableDef>
): Signal<TableRows<TableDef>> {
assertInInjectionContext(injectTable);

type UseTableRowType = RowType<TableDef>;

const conn = injectSpacetimeDB();
const connState = inject(SPACETIMEDB_CONNECTION);

const tableName = tableDef.name;
const accessorName = tableDef.accessorName;
Expand All @@ -93,11 +109,17 @@ export function injectTable<TableDef extends UntypedTableDef>(
// in order to keep behavior consistent across frameworks.

const computeSnapshot = (): readonly RowTypeDef<TableDef>[] => {
if (!conn.isActive) {
const state = connState();
if (!state.isActive) {
return [];
}

const connection = state.getConnection();
if (!connection) {
return [];
}

const table = conn.db[accessorName];
const table = connection.db[accessorName];

if (whereClause) {
return Array.from(table.iter()).filter(row =>
Expand All @@ -116,11 +138,17 @@ export function injectTable<TableDef extends UntypedTableDef>(
};

effect(onCleanup => {
if (!conn.isActive) {
const state = connState();
if (!state.isActive) {
return;
}

const table = conn.db[accessorName];
const connection = state.getConnection();
if (!connection) {
return;
}

const table = connection.db[accessorName];

const onInsert = (
ctx: EventContextInterface<UntypedRemoteModule>,
Expand Down Expand Up @@ -185,19 +213,12 @@ export function injectTable<TableDef extends UntypedTableDef>(
table.onDelete(onDelete);
table.onUpdate?.(onUpdate);

const subscription = conn
const subscription = connection
.subscriptionBuilder()
.onApplied(() => {
subscribeApplied = true;
updateSnapshot();
})
.onError(err => {
tableSignal.set({
rows: [],
isLoading: false,
error: err.event,
});
})
.subscribe(query);

onCleanup(() => {
Expand Down
2 changes: 1 addition & 1 deletion crates/bindings-typescript/src/angular/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { provideSpacetimeDB } from './provide-spacetimedb';
export { provideSpacetimeDB } from './provide-spacetimedb.ts';
Loading
Loading