Skip to content

v1.11.0 - Spacetime Generate producing duplicate import/exports #3869

Description

@sheibeck

I have typescript module and client. I'm generating my client modules by by running the generate command:

spacetime generate --lang typescript --out-dir ../client/src/module_bindings --project-path .

The results of this are putting duplicate import/exports into my index.ts file

Here's the contents of what's being generated. You can see that

// Import and reexport all reducer arg types

and the

// Import and reexport all types

have some of the exact same import/exports names.

// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.

// This was generated using spacetimedb cli version 1.11.0 (commit 49ccb7ac2ae18042057c844591c9749532bc691a).

/* eslint-disable */
/* tslint:disable */
import {
  DbConnectionBuilder as __DbConnectionBuilder,
  DbConnectionImpl as __DbConnectionImpl,
  SubscriptionBuilderImpl as __SubscriptionBuilderImpl,
  TypeBuilder as __TypeBuilder,
  convertToAccessorMap as __convertToAccessorMap,
  procedureSchema as __procedureSchema,
  procedures as __procedures,
  reducerSchema as __reducerSchema,
  reducers as __reducers,
  schema as __schema,
  t as __t,
  table as __table,
  type AlgebraicTypeType as __AlgebraicTypeType,
  type DbConnectionConfig as __DbConnectionConfig,
  type ErrorContextInterface as __ErrorContextInterface,
  type Event as __Event,
  type EventContextInterface as __EventContextInterface,
  type Infer as __Infer,
  type ReducerEventContextInterface as __ReducerEventContextInterface,
  type RemoteModule as __RemoteModule,
  type SubscriptionEventContextInterface as __SubscriptionEventContextInterface,
  type SubscriptionHandleImpl as __SubscriptionHandleImpl,
} from "spacetimedb";

// Import and reexport all reducer arg types
import Init from "./init_reducer";
export { Init };
import OnDisconnect from "./on_disconnect_reducer";
export { OnDisconnect };
import OnConnect from "./on_connect_reducer";
export { OnConnect };
import ApplyIntent from "./apply_intent_reducer";
export { ApplyIntent };
import Tick from "./tick_reducer";
export { Tick };

// Import and reexport all procedure arg types

// Import and reexport all table handle types
import CharactersRow from "./characters_table";
export { CharactersRow };
import NarrativeEventsRow from "./narrative_events_table";
export { NarrativeEventsRow };
import SessionsRow from "./sessions_table";
export { SessionsRow };
import UsersRow from "./users_table";
export { UsersRow };

// Import and reexport all types
import ApplyIntent from "./apply_intent_type";
export { ApplyIntent };
import Characters from "./characters_type";
export { Characters };
import Init from "./init_type";
export { Init };
import NarrativeEvents from "./narrative_events_type";
export { NarrativeEvents };
import OnConnect from "./on_connect_type";
export { OnConnect };
import OnDisconnect from "./on_disconnect_type";
export { OnDisconnect };
import Sessions from "./sessions_type";
export { Sessions };
import Tick from "./tick_type";
export { Tick };
import Users from "./users_type";
export { Users };

/** The schema information for all tables in this module. This is defined the same was as the tables would have been defined in the server. */
const tablesSchema = __schema(
  __table({
    name: 'characters',
    indexes: [
    ],
    constraints: [
    ],
  }, CharactersRow),
  __table({
    name: 'narrative_events',
    indexes: [
    ],
    constraints: [
    ],
  }, NarrativeEventsRow),
  __table({
    name: 'sessions',
    indexes: [
    ],
    constraints: [
    ],
  }, SessionsRow),
  __table({
    name: 'users',
    indexes: [
      { name: 'email', algorithm: 'btree', columns: [
        'email',
      ] },
      { name: 'id', algorithm: 'btree', columns: [
        'id',
      ] },
      { name: 'byProviderSub', algorithm: 'btree', columns: [
        'providerSub',
      ] },
    ],
    constraints: [
      { name: 'users_email_key', constraint: 'unique', columns: ['email'] },
      { name: 'users_id_key', constraint: 'unique', columns: ['id'] },
    ],
  }, UsersRow),
);

/** The schema information for all reducers in this module. This is defined the same way as the reducers would have been defined in the server, except the body of the reducer is omitted in code generation. */
const reducersSchema = __reducers(
  __reducerSchema("init", Init),
  __reducerSchema("apply_intent", ApplyIntent),
  __reducerSchema("tick", Tick),
);

/** The schema information for all procedures in this module. This is defined the same way as the procedures would have been defined in the server. */
const proceduresSchema = __procedures(
);

/** The remote SpacetimeDB module schema, both runtime and type information. */
const REMOTE_MODULE = {
  versionInfo: {
    cliVersion: "1.11.0" as const,
  },
  tables: tablesSchema.schemaType.tables,
  reducers: reducersSchema.reducersType.reducers,
  ...proceduresSchema,
} satisfies __RemoteModule<
  typeof tablesSchema.schemaType,
  typeof reducersSchema.reducersType,
  typeof proceduresSchema
>;

/** The tables available in this remote SpacetimeDB module. */
export const tables = __convertToAccessorMap(tablesSchema.schemaType.tables);

/** The reducers available in this remote SpacetimeDB module. */
export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reducers);

/** The context type returned in callbacks for all possible events. */
export type EventContext = __EventContextInterface<typeof REMOTE_MODULE>;
/** The context type returned in callbacks for reducer events. */
export type ReducerEventContext = __ReducerEventContextInterface<typeof REMOTE_MODULE>;
/** The context type returned in callbacks for subscription events. */
export type SubscriptionEventContext = __SubscriptionEventContextInterface<typeof REMOTE_MODULE>;
/** The context type returned in callbacks for error events. */
export type ErrorContext = __ErrorContextInterface<typeof REMOTE_MODULE>;
/** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */
export type SubscriptionHandle = __SubscriptionHandleImpl<typeof REMOTE_MODULE>;

/** Builder class to configure a new subscription to the remote SpacetimeDB instance. */
export class SubscriptionBuilder extends __SubscriptionBuilderImpl<typeof REMOTE_MODULE> {}

/** Builder class to configure a new database connection to the remote SpacetimeDB instance. */
export class DbConnectionBuilder extends __DbConnectionBuilder<DbConnection> {}

/** The typed database connection to manage connections to the remote SpacetimeDB instance. This class has type information specific to the generated module. */
export class DbConnection extends __DbConnectionImpl<typeof REMOTE_MODULE> {
  /** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */
  static builder = (): DbConnectionBuilder => {
    return new DbConnectionBuilder(REMOTE_MODULE, (config: __DbConnectionConfig<typeof REMOTE_MODULE>) => new DbConnection(config));
  };

  /** Creates a new {@link SubscriptionBuilder} to configure a subscription to the remote SpacetimeDB instance. */
  subscriptionBuilder = (): SubscriptionBuilder => {
    return new SubscriptionBuilder(this);
  };
}


Here's my index.ts from my server module. It's pretty simple with just a few tables and reducers

import { schema, table, t, SenderError } from "spacetimedb/server";
import { handleIntent } from './intent-handler';

// Helper to get current user
function getCurrentUser(ctx: any): any {
    if (!ctx.sender) throw new SenderError('Unauthenticated');

    let user: any = null;
    for (const u of ctx.db.users.id.filter(ctx.sender)) {
        user = u;
        break;
    }

    if (!user) throw new SenderError('User not found');
    return user;
}

export const spacetimedb = schema(
    // Auth users table per Google OAuth -> SpacetimeDB identity flow
    table(
        {
            name: "users", public: true,
            indexes: [
                { name: 'byProviderSub', algorithm: 'btree', columns: ['provider_sub'] }
            ]
        },
        {
            id: t.identity().primaryKey(),
            provider: t.string(),
            provider_sub: t.string(),
            email: t.string().unique(),
            created_at: t.number(),
            online: t.bool()
        }
    ),
    table(
        { name: "characters" },
        {
            id: t.string(),
            owner_id: t.string(),
            class: t.string(),
            stats_json: t.string()
        }
    ),
    table(
        { name: "sessions" },
        {
            account_id: t.identity(),
            device_id: t.string(),
            last_seen: t.number()
        }
    ),
    table(
        { name: "narrative_events", public: true },
        {
            id: t.string(),
            character_id: t.string(),
            text: t.string(),
            intent_json: t.string(),
            timestamp: t.number()
        }
    )
);

// lifecycle reducers
spacetimedb.reducer("init", (_ctx) => {
    // Called when the module is initially published
});

function safeStringify(obj: any) {
    return JSON.stringify(
        obj,
        (_key, value) =>
            typeof value === "bigint" ? value.toString() : value,
        2
    );
}

// Application reducers
// ensure_user: inserts or fetches a user based on email
spacetimedb.clientDisconnected(_ctx => {
    const identity = _ctx.sender;
    if (!identity) throw new SenderError('Unauthenticated');

    const user = _ctx.db.users.id.find(identity);
    if (user) {
        user.online = false;
        _ctx.db.users.id.update(user);
    }
});

spacetimedb.clientConnected((ctx) => {
    const jwt = ctx.senderAuth.jwt;

    //throw new SenderError("DEBUG ctx.SenderAuth: " + safeStringify(ctx));

    if (jwt == null) {
        throw new SenderError("Unauthorized: JWT is required to connect");
    }

    const payload = (jwt as any)?.fullPayload ?? {};
    const emailClaim = typeof payload.email === 'string' ? payload.email : undefined;
    const email = emailClaim ?? (jwt.subject ? `${jwt.subject}@placeholder.local` : undefined);

    console.log(`Client connected with sub: ${jwt.subject}, iss: ${jwt.issuer}, email: ${email ?? 'missing'}`);

    if (!email) {
        throw new SenderError('Unauthorized: Email claim is required');
    }

    // Prefer lookup by email; only insert if email doesn't exist
    const user = ctx.db.users.id.find(ctx.sender);
    if (user) {
        ctx.db.users.id.update({ ...user, online: true });
    }
    else {
        // No existing user for this email; create new bound to current identity
        ctx.db.users.insert({
            id: ctx.sender,
            provider: 'google',
            provider_sub: jwt.subject ?? '',
            email,
            created_at: Date.now(),
            online: true,
        });
    }
});

spacetimedb.reducer('apply_intent', { intent_json: t.string() }, (ctx, { intent_json }) => {
    // Ensure user is authenticated and has valid email
    const user = getCurrentUser(ctx);
    if (!user.email) {
        throw new SenderError('User email not verified');
    }

    let parsed: any = null;
    try {
        parsed = JSON.parse(intent_json);
    } catch (e) {
        console.log('apply_intent: invalid JSON');
        return;
    }
    const result = handleIntent(parsed);
    const event = {
        id: `${Date.now()}`,
        character_id: result.characterId ?? 'unknown',
        text: result.narrativeText,
        intent_json,
        timestamp: Date.now()
    };

    // Use camelCase table accessor generated by SpacetimeDB
    ctx.db.narrative_events.insert(event);
    console.log(`apply_intent: ${event.text} by ${user.email}`);
});

spacetimedb.reducer('tick', (ctx) => {
    // TODO: World tick logic (AI, combat resolution, NPC actions)
    console.log('tick: world updated');
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions