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
10 changes: 10 additions & 0 deletions crates/bindings-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@
"import": "./dist/server/index.mjs",
"require": "./dist/server/index.cjs",
"default": "./dist/server/index.mjs"
},
"./vue": {
"types": "./dist/vue/index.d.ts",
"import": "./dist/vue/index.mjs",
"require": "./dist/vue/index.cjs",
"default": "./dist/vue/index.mjs"
}
},
"size-limit": [
Expand Down Expand Up @@ -161,12 +167,16 @@
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0-0 || ^19.0.0",
"vue": "^3.3.0",
"undici": "^6.19.2"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"vue": {
"optional": true
},
"undici": {
"optional": true
}
Expand Down
156 changes: 156 additions & 0 deletions crates/bindings-typescript/src/vue/SpacetimeDBProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import {
defineComponent,
onMounted,
onUnmounted,
provide,
reactive,
shallowRef,
type PropType,
type Slot,
} from 'vue';
import {
DbConnectionBuilder,
type DbConnectionImpl,
type ErrorContextInterface,
type RemoteModuleOf,
} from '../sdk/db_connection_impl';
import { ConnectionId } from '../lib/connection_id';
import {
SPACETIMEDB_INJECTION_KEY,
type ConnectionState,
} from './connection_state';

export interface SpacetimeDBProviderProps<
DbConnection extends DbConnectionImpl<any>,
> {
connectionBuilder: DbConnectionBuilder<DbConnection>;
}

function setupConnection<DbConnection extends DbConnectionImpl<any>>(
connectionBuilder: DbConnectionBuilder<DbConnection>
): {
state: ConnectionState;
cleanup: () => void;
} {
const connRef = shallowRef<DbConnection | null>(null);
let cleanupTimeoutId: ReturnType<typeof setTimeout> | null = null;

const state = reactive<ConnectionState>({
isActive: false,
identity: undefined,
token: undefined,
connectionId: ConnectionId.random(),
connectionError: undefined,
getConnection: <T extends DbConnectionImpl<any>>() =>
connRef.value as T | null,
});

provide(SPACETIMEDB_INJECTION_KEY, state);

let onConnectCallback: ((conn: DbConnection) => void) | null = null;
let onDisconnectCallback:
| ((ctx: ErrorContextInterface<RemoteModuleOf<DbConnection>>) => void)
| null = null;
let onConnectErrorCallback:
| ((
ctx: ErrorContextInterface<RemoteModuleOf<DbConnection>>,
err: Error
) => void)
| null = null;

onMounted(() => {
if (cleanupTimeoutId) {
clearTimeout(cleanupTimeoutId);
cleanupTimeoutId = null;
}

if (!connRef.value) {
connRef.value = connectionBuilder.build();
}

onConnectCallback = (conn: DbConnection) => {
state.isActive = conn.isActive;
state.identity = conn.identity;
state.token = conn.token;
state.connectionId = conn.connectionId;
state.connectionError = undefined;
};

onDisconnectCallback = (
ctx: ErrorContextInterface<RemoteModuleOf<DbConnection>>
) => {
state.isActive = ctx.isActive;
};

onConnectErrorCallback = (
ctx: ErrorContextInterface<RemoteModuleOf<DbConnection>>,
err: Error
) => {
state.isActive = ctx.isActive;
state.connectionError = err;
};

connectionBuilder.onConnect(onConnectCallback);
connectionBuilder.onDisconnect(onDisconnectCallback);
connectionBuilder.onConnectError(onConnectErrorCallback);

const conn = connRef.value;
if (conn) {
state.isActive = conn.isActive;
state.identity = conn.identity;
state.token = conn.token;
state.connectionId = conn.connectionId;
}
});

const cleanup = () => {
if (connRef.value) {
if (onConnectCallback) {
connRef.value.removeOnConnect?.(onConnectCallback as any);
}
if (onDisconnectCallback) {
connRef.value.removeOnDisconnect?.(onDisconnectCallback as any);
}
if (onConnectErrorCallback) {
connRef.value.removeOnConnectError?.(onConnectErrorCallback as any);
}

cleanupTimeoutId = setTimeout(() => {
connRef.value?.disconnect();
connRef.value = null;
cleanupTimeoutId = null;
}, 0);
}
};

onUnmounted(cleanup);

return { state, cleanup };
}

export const SpacetimeDBProvider = defineComponent({
name: 'SpacetimeDBProvider',

props: {
connectionBuilder: {
type: Object as PropType<DbConnectionBuilder<any>>,
required: true,
},
},

setup(props, { slots }) {
setupConnection(props.connectionBuilder);

return () => {
const defaultSlot = slots.default as Slot | undefined;
return defaultSlot ? defaultSlot() : null;
};
},
});

export function useSpacetimeDBProvider<
DbConnection extends DbConnectionImpl<any>,
>(connectionBuilder: DbConnectionBuilder<DbConnection>): ConnectionState {
const { state } = setupConnection(connectionBuilder);
return state;
}
19 changes: 19 additions & 0 deletions crates/bindings-typescript/src/vue/connection_state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { InjectionKey } from 'vue';
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_INJECTION_KEY = Symbol(
'spacetimedb'
) as InjectionKey<ConnectionState>;
4 changes: 4 additions & 0 deletions crates/bindings-typescript/src/vue/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './SpacetimeDBProvider.ts';
export { useSpacetimeDB } from './useSpacetimeDB.ts';
export { useTable, where, eq } from './useTable.ts';
export { useReducer } from './useReducer.ts';
56 changes: 56 additions & 0 deletions crates/bindings-typescript/src/vue/useReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { shallowRef, watch, onUnmounted } from 'vue';
import { useSpacetimeDB } from './useSpacetimeDB';
import type { InferTypeOfRow } from '../lib/type_builders';
import type { UntypedReducerDef } from '../sdk/reducers';
import type { Prettify } from '../lib/type_util';

type IsEmptyObject<T> = [keyof T] extends [never] ? true : false;
type MaybeParams<T> = IsEmptyObject<T> extends true ? [] : [params: T];

type ParamsType<R extends UntypedReducerDef> = MaybeParams<
Prettify<InferTypeOfRow<R['params']>>
>;

export function useReducer<ReducerDef extends UntypedReducerDef>(
reducerDef: ReducerDef
): (...params: ParamsType<ReducerDef>) => void {
const conn = useSpacetimeDB();
const reducerName = reducerDef.accessorName;

const queueRef = shallowRef<ParamsType<ReducerDef>[]>([]);

const stopWatch = watch(
() => conn.isActive,
() => {
const connection = conn.getConnection();
if (!connection) return;

const fn = (connection.reducers as any)[reducerName] as (
...p: ParamsType<ReducerDef>
) => void;
if (queueRef.value.length) {
const pending = queueRef.value.splice(0);
for (const params of pending) {
fn(...params);
}
}
},
{ immediate: true }
);

onUnmounted(() => {
stopWatch();
});

return (...params: ParamsType<ReducerDef>) => {
const connection = conn.getConnection();
if (!connection) {
queueRef.value.push(params);
return;
}
const fn = (connection.reducers as any)[reducerName] as (
...p: ParamsType<ReducerDef>
) => void;
fn(...params);
};
}
18 changes: 18 additions & 0 deletions crates/bindings-typescript/src/vue/useSpacetimeDB.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { inject } from 'vue';
import {
SPACETIMEDB_INJECTION_KEY,
type ConnectionState,
} from './connection_state';

export function useSpacetimeDB(): ConnectionState {
const context = inject(SPACETIMEDB_INJECTION_KEY);

if (!context) {
throw new Error(
'useSpacetimeDB must be used within a SpacetimeDBProvider component. ' +
'Did you forget to add a `SpacetimeDBProvider` to your component tree?'
);
}

return context;
}
Loading
Loading