@warpfx/server 0.2.1 → 0.3.1

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +141 -163
  2. package/dist/index.js +34 -29
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -66,10 +66,20 @@ interface ColumnDef {
66
66
  /** Default value for this column when inserting a row without it. */
67
67
  default?: unknown;
68
68
  }
69
+ /** Table access mode for client sync. */
70
+ type AccessMode = "private" | "public";
69
71
  /** Table definition in a warp.schema.json. */
70
72
  interface TableDef {
71
73
  /** Column definitions keyed by column name. */
72
74
  columns: Record<string, ColumnDef>;
75
+ /**
76
+ * Client access mode — controls automatic sync to players.
77
+ *
78
+ * - `"private"` — Only the owning player receives their rows (scope = primary key).
79
+ * - `"public"` — All connected players receive all rows.
80
+ * - Omitted — Server-only, not synced to clients.
81
+ */
82
+ access?: AccessMode;
73
83
  }
74
84
  /** The full warp.schema.json format. */
75
85
  interface WarpSchema {
@@ -84,22 +94,22 @@ interface WarpSchema {
84
94
  }
85
95
 
86
96
  /**
87
- * @warp/server — Typed SDK for Warp server-side module development.
97
+ * @warpfx/server — Server-side SDK for Warp module development.
88
98
  *
89
- * Provides type-safe access to the Warp core API. Under the hood,
90
- * all calls go through FiveM's resource exports to the 'warp' resource.
99
+ * Clean, minimal API for building FiveM server features with Warp.
100
+ * All calls delegate to the Warp core via FiveM resource exports.
91
101
  *
92
102
  * Usage:
93
- * import { onReady, provide, use, getEvents } from '@warp/server';
103
+ * import { action, fn, command, error, table } from '@warpfx/server';
104
+ * import { accounts } from './db'; // auto-generated from schema
94
105
  *
95
- * onReady(() => {
96
- * const events = getEvents();
97
- * events.on('player:spawned', (data) => { ... });
98
- * provide('economy', new EconomyService());
106
+ * action('deposit', async (player, { amount }) => {
107
+ * const account = accounts.find(player.citizenId);
108
+ * accounts.update(player.citizenId, { balance: account.balance + amount });
99
109
  * });
100
110
  */
101
- /** Player lifecycle event data. */
102
- interface PlayerEventContext {
111
+ /** Player context passed to action/function handlers. */
112
+ interface Player {
103
113
  /** FiveM server-side player ID. */
104
114
  source: number;
105
115
  /** Citizen ID (license identifier). */
@@ -124,216 +134,184 @@ interface PlayerRow {
124
134
  type PlayerState = "connecting" | "connected" | "spawned";
125
135
  /** Player entity with ECS component access. */
126
136
  interface PlayerEntity {
127
- /** FiveM server-side player ID. */
128
137
  readonly source: number;
129
- /** Citizen ID (license identifier). */
130
138
  readonly citizenId: string;
131
- /** Display name. */
132
139
  readonly name: string;
133
- /** Discord identifier (e.g. "discord:123456789"), if linked. */
134
140
  readonly discordId: string | undefined;
135
- /** Steam identifier (e.g. "steam:110000..."), if linked. */
136
141
  readonly steamId: string | undefined;
137
- /** Current lifecycle state. */
138
142
  readonly state: PlayerState;
139
- /** Get a component attached to this player. */
140
143
  getComponent<T>(component: string): T | undefined;
141
- /** Attach or replace a component on this player. */
142
144
  setComponent<T>(component: string, data: T): void;
143
- /** Check if this player has a specific component. */
144
145
  hasComponent(component: string): boolean;
145
- /** Remove a component from this player. */
146
146
  removeComponent(component: string): boolean;
147
147
  }
148
148
  /** Typed event bus for server-side and network communication. */
149
149
  interface ServerEventBus<TMap extends Record<string, any> = Record<string, any>> {
150
- /** Subscribe to a local event. Returns an unsubscribe function. */
151
150
  on<K extends string & keyof TMap>(event: K, handler: (data: TMap[K]) => void): () => void;
152
- /** Unsubscribe from a local event. */
153
151
  off<K extends string & keyof TMap>(event: K, handler: (data: TMap[K]) => void): void;
154
- /** Emit a local event to all subscribers. */
155
152
  emit<K extends string & keyof TMap>(event: K, data: TMap[K]): void;
156
- /** Send an event to a specific client. */
157
153
  emitClient<K extends string & keyof TMap>(event: K, target: number, data: TMap[K]): void;
158
- /** Broadcast an event to all connected clients. */
159
154
  emitAllClients<K extends string & keyof TMap>(event: K, data: TMap[K]): void;
160
- /** Listen for events from clients. Returns an unsubscribe function. */
161
155
  onClient<K extends string & keyof TMap>(event: K, handler: (source: number, data: TMap[K]) => void): () => void;
162
- /** Unsubscribe from a client event. */
163
156
  offClient<K extends string & keyof TMap>(event: K, handler: (source: number, data: TMap[K]) => void): void;
164
157
  }
165
158
  /** ECS-inspired entity component store. */
166
159
  interface EntityStore {
167
- /** Attach a component to an entity. */
168
160
  attach<T>(entityId: string, component: string, data: T): void;
169
- /** Get a component from an entity. Returns undefined if not found. */
170
161
  get<T>(entityId: string, component: string): T | undefined;
171
- /** Remove a component from an entity. */
172
162
  detach(entityId: string, component: string): boolean;
173
- /** Remove all components for an entity. */
174
163
  removeEntity(entityId: string): void;
175
- /** Check if an entity has a specific component. */
176
164
  has(entityId: string, component: string): boolean;
177
- /** Iterate all entities that have a specific component. */
178
165
  query<T>(component: string): IterableIterator<[string, T]>;
179
166
  }
180
- /**
181
- * Register a callback to run when Warp is fully booted.
182
- * If Warp is already ready, the callback fires immediately.
183
- * All other SDK functions should be called inside this callback.
184
- */
185
- declare function onReady(cb: () => void): void;
186
- /** Check if Warp has finished booting. */
187
- declare function isReady(): boolean;
188
- /** Register a service that other modules can resolve via use(). */
189
- declare function provide<T>(token: string, service: T): void;
190
- /** Resolve a service registered by another module. */
191
- declare function use<T = unknown>(token: string): T;
192
- /** Get the shared event bus instance. */
193
- declare function getEvents<TMap extends Record<string, any> = Record<string, any>>(): ServerEventBus<TMap>;
194
- /** Get the shared entity component store. */
195
- declare function getEntities(): EntityStore;
196
- /** Find a player by citizenId from the database cache. */
197
- declare function getPlayer(citizenId: string): PlayerRow | null;
198
- /** Get all cached players. */
199
- declare function getPlayers(): PlayerRow[];
200
- /**
201
- * Get the player entity for a citizenId.
202
- * Returns the entity with lifecycle state and component access.
203
- */
204
- declare function getEntity(citizenId: string): PlayerEntity | null;
205
- /**
206
- * Transition a player to the "spawned" state.
207
- * Call after character selection (e.g. from a multichar module).
208
- * Fires all onPlayerSpawned hooks.
209
- */
210
- declare function spawnPlayer(citizenId: string): void;
211
- /** Register a callback for when a player connects (database confirmed). */
212
- declare function onPlayerConnect(cb: (player: PlayerEventContext) => void): void;
213
- /** Register a callback for when a player spawns (character selected). */
214
- declare function onPlayerSpawned(cb: (player: PlayerEventContext) => void): void;
215
- /** Register a callback for when a player disconnects. */
216
- declare function onPlayerDisconnect(cb: (player: PlayerEventContext) => void): void;
217
- /**
218
- * Get the database bridge for direct table and reducer access.
219
- * Advanced — most modules should use the event bus and DI instead.
220
- */
221
- declare function getBridge(): any;
222
- /** Subscribe to database tables. Call inside onReady(). */
223
- declare function subscribe(queries: string[]): Promise<void>;
224
- /** Sync configuration for a table. */
225
- interface SyncConfig {
226
- /** Who receives updates for this table. */
227
- to: "owner" | "all";
228
- /** Column name for owner filtering (required when to: 'owner'). */
229
- scope?: string;
230
- /** Primary key column for row identity (defaults to 'id'). */
231
- key?: string;
167
+ /** Table handle for typed database operations. */
168
+ interface Table<T = any> {
169
+ /** Find a row by primary key. */
170
+ find(primaryKey: any): T | null;
171
+ /** Get all rows in the table. */
172
+ all(): T[];
173
+ /** Insert a new row. */
174
+ insert(data: T): Promise<void>;
175
+ /** Update an existing row by primary key (partial update). */
176
+ update(primaryKey: any, data: Partial<T>): Promise<void>;
177
+ /** Delete a row by primary key. */
178
+ delete(primaryKey: any): Promise<void>;
232
179
  }
233
- /** Context passed to action handlers. */
234
- interface ActionContext {
235
- /** FiveM server-side player ID. */
236
- source: number;
237
- /** Player's citizen ID (license identifier). */
238
- citizenId: string;
239
- /** Player's display name. */
180
+ /** Parameter hint shown in command autocomplete. */
181
+ interface CommandParam {
240
182
  name: string;
241
- /** Discord identifier (e.g. "discord:123456789"), if linked. */
242
- discordId?: string;
243
- /** Steam identifier (e.g. "steam:110000..."), if linked. */
244
- steamId?: string;
245
- /** Database table accessors (read-only). */
246
- db: any;
247
- /** Database reducer proxy (for mutations). */
248
- reducers: any;
183
+ help?: string;
184
+ }
185
+ /** Options for command(). */
186
+ interface CommandOptions {
187
+ description?: string;
188
+ ace?: string;
189
+ canUse?: (source: number) => boolean;
190
+ params?: CommandParam[];
191
+ }
192
+ /** Action error received from the server. */
193
+ interface ActionError {
194
+ action: string;
195
+ code: string;
196
+ message: string;
197
+ }
198
+ /** Describes a single table row change. */
199
+ interface TableChange<T = any> {
200
+ /** The type of change. */
201
+ type: "insert" | "update" | "delete";
202
+ /** The current row (new on insert/update, deleted row on delete). */
203
+ row: T;
204
+ /** The previous row (on update only, undefined otherwise). */
205
+ oldRow?: T;
249
206
  }
250
- /** Action handler function. */
251
- type ActionHandler = (ctx: ActionContext, payload: any) => Promise<void> | void;
252
- /** Server function handler — returns a value back to the caller. */
253
- type FunctionHandler<T = any> = (ctx: ActionContext, payload: any) => Promise<T> | T;
254
207
  /**
255
- * Register a table for automatic sync to clients.
208
+ * Register an action handler that clients can invoke.
256
209
  *
257
- * The sync engine subscribes to SpacetimeDB table changes and routes
258
- * updates to the correct players based on the config. Call inside onReady().
210
+ * Actions are fire-and-forget mutations the client sends data,
211
+ * the server validates and mutates, state syncs back automatically.
259
212
  *
260
213
  * @example
261
- * registerSync('inventory', { to: 'owner', scope: 'ownerId', key: 'id' });
262
- * registerSync('itemDefinitions', { to: 'all', key: 'name' });
214
+ * action('deposit', async (player, { amount }) => {
215
+ * const account = accounts.find(player.citizenId);
216
+ * accounts.update(player.citizenId, { balance: account.balance + amount });
217
+ * });
263
218
  */
264
- declare function registerSync(table: string, config: SyncConfig): void;
219
+ declare function action(name: string, handler: (player: Player, payload: any) => Promise<void> | void): void;
265
220
  /**
266
- * Register an action handler that clients can invoke via useAction().
221
+ * Register a server function that clients can call and await a result.
267
222
  *
268
- * Actions are the only entry point for client-initiated mutations.
269
- * The sync engine handles security, error routing, and state updates.
223
+ * Unlike actions (fire-and-forget), functions return a value to the caller.
224
+ * Use for request/response patterns like data fetches or validations.
270
225
  *
271
226
  * @example
272
- * registerAction('inventory:moveItem', async (ctx, { itemId, toSlot }) => {
273
- * await ctx.reducers.moveItem(ctx.citizenId, itemId, toSlot);
227
+ * fn('getBalance', async (player, { targetId }) => {
228
+ * const account = accounts.find(targetId);
229
+ * return { balance: account?.balance ?? 0 };
274
230
  * });
275
231
  */
276
- declare function registerAction(name: string, handler: ActionHandler): void;
232
+ declare function fn<T = any>(name: string, handler: (player: Player, payload: any) => Promise<T> | T): void;
277
233
  /**
278
- * Register a server function that clients can call and await a result.
279
- *
280
- * Unlike actions (fire-and-forget mutations), functions return a value
281
- * to the caller. Use for request/response patterns like data fetches,
282
- * calculations, or validations that need a result.
234
+ * Register a chat command with optional autocomplete.
283
235
  *
284
236
  * @example
285
- * registerFunction('economy:getBalance', async (ctx) => {
286
- * const account = ctx.db.economy_accounts.citizenId.find(ctx.citizenId);
287
- * return { balance: account?.balance ?? 0 };
237
+ * command('balance', (player) => {
238
+ * const account = accounts.find(player.citizenId);
239
+ * player.notify(`Balance: $${account.balance}`);
288
240
  * });
241
+ *
242
+ * command('give', {
243
+ * description: 'Give money to a player',
244
+ * params: [{ name: 'id' }, { name: 'amount' }],
245
+ * }, (player, args) => { ... });
289
246
  */
290
- declare function registerFunction<T = any>(name: string, handler: FunctionHandler<T>): void;
247
+ declare function command(name: string, handlerOrOptions: CommandOptions | ((player: Player, args: string[], raw: string) => void), handler?: (player: Player, args: string[], raw: string) => void): void;
291
248
  /**
292
- * Typed error for action handlers. Thrown errors with a `code` property
293
- * are sent to the client as structured error messages.
249
+ * Create an error to throw from action/function handlers.
250
+ * The error is sent back to the client with a structured code and message.
294
251
  *
295
252
  * @example
296
- * throw new WarpError('ITEM_NOT_FOUND', 'That item no longer exists');
253
+ * throw error('NO_ACCOUNT', 'No account found');
297
254
  */
298
- declare class WarpError extends Error {
299
- code: string;
300
- constructor(code: string, message?: string);
301
- }
302
- /** Parameter hint shown in command autocomplete. */
303
- interface CommandParam {
304
- /** Parameter name displayed in the suggestion (e.g. "id", "reason"). */
305
- name: string;
306
- /** Optional help text for the parameter. */
307
- help?: string;
308
- }
309
- /** Options for registerCommand(). */
310
- interface CommandOptions {
311
- /** Short description shown in autocomplete. */
312
- description?: string;
313
- /** ACE permission required — players without it won't see the command. */
314
- ace?: string;
315
- /** Custom permission predicate (overrides ace check when provided). */
316
- canUse?: (source: number) => boolean;
317
- /** Parameter hints shown in autocomplete. */
318
- params?: CommandParam[];
319
- }
255
+ declare function error(code: string, message?: string): Error;
256
+ /**
257
+ * Get a table handle for database operations.
258
+ *
259
+ * The table name must be the full prefixed name (e.g. 'economy_accounts').
260
+ * For auto-generated handles with types, use the generated `db.ts` instead.
261
+ *
262
+ * @example
263
+ * const accounts = table<Account>('economy_accounts');
264
+ * const account = accounts.find(citizenId);
265
+ * await accounts.update(citizenId, { balance: 500 });
266
+ */
267
+ declare function table<T = any>(name: string): Table<T>;
320
268
  /**
321
- * Register a command with autocomplete support.
269
+ * React to any change in a database table.
322
270
  *
323
- * Players see matching commands as they type in the chat input,
324
- * filtered by permission. The handler receives the standard FiveM
325
- * command arguments.
271
+ * Fires on insert, update, or delete regardless of what caused the change
272
+ * (action, another module, migration, Studio edit). Works on all tables
273
+ * including server-only tables without an `access` field.
326
274
  *
327
275
  * @example
328
- * registerCommand('arrest', {
329
- * description: 'Arrest a nearby player',
330
- * ace: 'police.arrest',
331
- * params: [{ name: 'id', help: 'Player server ID' }],
332
- * }, (source, args) => {
333
- * const targetId = parseInt(args[0], 10);
334
- * // ...
276
+ * onTableChange<Account>('economy_accounts', (change) => {
277
+ * if (change.type === 'update' && change.row.balance < 0) {
278
+ * accounts.update(change.row.citizenId, { frozen: true });
279
+ * }
335
280
  * });
336
281
  */
337
- declare function registerCommand(name: string, options: CommandOptions, handler: (source: number, args: string[], rawCommand: string) => void): void;
282
+ declare function onTableChange<T = any>(table: string, handler: (change: TableChange<T>) => void): void;
283
+ /**
284
+ * Register a callback to run when Warp is fully booted.
285
+ * If Warp is already ready, the callback fires immediately.
286
+ */
287
+ declare function onReady(cb: () => void): void;
288
+ /** Check if Warp has finished booting. */
289
+ declare function isReady(): boolean;
290
+ /** Register a service that other modules can resolve via use(). */
291
+ declare function provide<T>(token: string, service: T): void;
292
+ /** Resolve a service registered by another module. */
293
+ declare function use<T = unknown>(token: string): T;
294
+ /** Get the shared event bus instance. */
295
+ declare function getEvents<TMap extends Record<string, any> = Record<string, any>>(): ServerEventBus<TMap>;
296
+ /** Get the shared entity component store. */
297
+ declare function getEntities(): EntityStore;
298
+ /** Find a player by citizenId from the database cache. */
299
+ declare function getPlayer(citizenId: string): PlayerRow | null;
300
+ /** Get all cached players. */
301
+ declare function getPlayers(): PlayerRow[];
302
+ /** Get the player entity for a citizenId. */
303
+ declare function getEntity(citizenId: string): PlayerEntity | null;
304
+ /** Transition a player to the "spawned" state. */
305
+ declare function spawnPlayer(citizenId: string): void;
306
+ /** Register a callback for when a player connects. */
307
+ declare function onPlayerConnect(cb: (player: Player) => void): void;
308
+ /** Register a callback for when a player spawns. */
309
+ declare function onPlayerSpawned(cb: (player: Player) => void): void;
310
+ /** Register a callback for when a player disconnects. */
311
+ declare function onPlayerDisconnect(cb: (player: Player) => void): void;
312
+ /** Get the database bridge for direct table and reducer access. */
313
+ declare function getBridge(): any;
314
+ /** Subscribe to database tables. */
315
+ declare function subscribe(queries: string[]): Promise<void>;
338
316
 
339
- export { type ActionContext, type ActionHandler, type ColumnDef, type ColumnType, type CommandOptions, type CommandParam, type EntityStore, type FunctionHandler, type PlayerEntity, type PlayerEventContext, type PlayerRow, type PlayerState, type ServerEventBus, type SyncConfig, type TableDef, WarpError, type WarpSchema, getBridge, getEntities, getEntity, getEvents, getPlayer, getPlayers, isReady, onPlayerConnect, onPlayerDisconnect, onPlayerSpawned, onReady, provide, registerAction, registerCommand, registerFunction, registerSync, spawnPlayer, subscribe, use };
317
+ export { type AccessMode, type ActionError, type ColumnDef, type ColumnType, type CommandOptions, type CommandParam, type EntityStore, type Player, type PlayerEntity, type PlayerRow, type PlayerState, type ServerEventBus, type Table, type TableChange, type TableDef, type WarpSchema, action, command, error, fn, getBridge, getEntities, getEntity, getEvents, getPlayer, getPlayers, isReady, onPlayerConnect, onPlayerDisconnect, onPlayerSpawned, onReady, onTableChange, provide, spawnPlayer, subscribe, table, use };
package/dist/index.js CHANGED
@@ -20,7 +20,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- WarpError: () => WarpError,
23
+ action: () => action,
24
+ command: () => command,
25
+ error: () => error,
26
+ fn: () => fn,
24
27
  getBridge: () => getBridge,
25
28
  getEntities: () => getEntities,
26
29
  getEntity: () => getEntity,
@@ -32,19 +35,39 @@ __export(index_exports, {
32
35
  onPlayerDisconnect: () => onPlayerDisconnect,
33
36
  onPlayerSpawned: () => onPlayerSpawned,
34
37
  onReady: () => onReady,
38
+ onTableChange: () => onTableChange,
35
39
  provide: () => provide,
36
- registerAction: () => registerAction,
37
- registerCommand: () => registerCommand,
38
- registerFunction: () => registerFunction,
39
- registerSync: () => registerSync,
40
40
  spawnPlayer: () => spawnPlayer,
41
41
  subscribe: () => subscribe,
42
+ table: () => table,
42
43
  use: () => use
43
44
  });
44
45
  module.exports = __toCommonJS(index_exports);
45
46
  function warp() {
46
47
  return globalThis.exports["warp"];
47
48
  }
49
+ function action(name, handler) {
50
+ warp().action(name, handler);
51
+ }
52
+ function fn(name, handler) {
53
+ warp().fn(name, handler);
54
+ }
55
+ function command(name, handlerOrOptions, handler) {
56
+ if (typeof handlerOrOptions === "function") {
57
+ warp().command(name, {}, handlerOrOptions);
58
+ } else {
59
+ warp().command(name, handlerOrOptions, handler);
60
+ }
61
+ }
62
+ function error(code, message) {
63
+ return warp().error(code, message);
64
+ }
65
+ function table(name) {
66
+ return warp().table(name);
67
+ }
68
+ function onTableChange(table2, handler) {
69
+ warp().onTableChange(table2, handler);
70
+ }
48
71
  function onReady(cb) {
49
72
  warp().onReady(cb);
50
73
  }
@@ -90,28 +113,12 @@ function getBridge() {
90
113
  function subscribe(queries) {
91
114
  return warp().subscribe(queries);
92
115
  }
93
- function registerSync(table, config) {
94
- warp().registerSync(table, config);
95
- }
96
- function registerAction(name, handler) {
97
- warp().registerAction(name, handler);
98
- }
99
- function registerFunction(name, handler) {
100
- warp().registerFunction(name, handler);
101
- }
102
- var WarpError = class extends Error {
103
- constructor(code, message) {
104
- super(message ?? code);
105
- this.code = code;
106
- this.name = "WarpError";
107
- }
108
- };
109
- function registerCommand(name, options, handler) {
110
- warp().registerCommand(name, options, handler);
111
- }
112
116
  // Annotate the CommonJS export names for ESM import in node:
113
117
  0 && (module.exports = {
114
- WarpError,
118
+ action,
119
+ command,
120
+ error,
121
+ fn,
115
122
  getBridge,
116
123
  getEntities,
117
124
  getEntity,
@@ -123,12 +130,10 @@ function registerCommand(name, options, handler) {
123
130
  onPlayerDisconnect,
124
131
  onPlayerSpawned,
125
132
  onReady,
133
+ onTableChange,
126
134
  provide,
127
- registerAction,
128
- registerCommand,
129
- registerFunction,
130
- registerSync,
131
135
  spawnPlayer,
132
136
  subscribe,
137
+ table,
133
138
  use
134
139
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warpfx/server",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Warp Framework SDK for server-side FiveM module development",
5
5
  "keywords": [
6
6
  "fivem",