@jgengine/convex 0.49.1 → 0.51.0

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.
@@ -13,8 +13,8 @@ export type ConvexBackendOptions<TRawPresenceRow extends {
13
13
  client: ConvexReactClient;
14
14
  gameId: string;
15
15
  userId: string;
16
- api?: ConvexGameApi;
17
- poseTuning?: PoseSyncTuning;
16
+ api?: ConvexGameApi | undefined;
17
+ poseTuning?: PoseSyncTuning | undefined;
18
18
  presence?: {
19
19
  functions: ConvexPresenceFunctions;
20
20
  mapRow: (row: TRawPresenceRow) => TPresenceRow;
@@ -15,23 +15,23 @@ export type ConvexGameApi = {
15
15
  runtime: {
16
16
  joinServer: FunctionReference<"mutation", "public", {
17
17
  gameId: string;
18
- serverId?: string;
18
+ serverId?: string | undefined;
19
19
  mode?: string;
20
20
  visibility?: "public" | "private";
21
21
  joinCode?: string;
22
- externalId?: string;
22
+ externalId?: string | undefined;
23
23
  sessionId?: string;
24
24
  }, JoinServerOutcome>;
25
25
  leaveServer: FunctionReference<"mutation", "public", {
26
26
  serverId: string;
27
- externalId?: string;
27
+ externalId?: string | undefined;
28
28
  sessionId?: string;
29
29
  }, null>;
30
30
  runCommand: FunctionReference<"mutation", "public", {
31
31
  serverId: string;
32
32
  command: string;
33
33
  input: unknown;
34
- externalId?: string;
34
+ externalId?: string | undefined;
35
35
  }, {
36
36
  ok: true;
37
37
  } | {
@@ -42,7 +42,7 @@ export type ConvexGameApi = {
42
42
  serverId: string;
43
43
  action: string;
44
44
  entry: unknown;
45
- externalId?: string;
45
+ externalId?: string | undefined;
46
46
  }, null>;
47
47
  getServer: FunctionReference<"query", "public", {
48
48
  serverId: string;
@@ -55,13 +55,13 @@ export type ConvexGameApi = {
55
55
  getFeed: FunctionReference<"query", "public", {
56
56
  serverId: string;
57
57
  action: string;
58
- externalId?: string;
58
+ externalId?: string | undefined;
59
59
  }, unknown[]>;
60
60
  };
61
61
  presence: {
62
62
  list: FunctionReference<"query", "public", {
63
63
  serverId: string;
64
- externalId?: string;
64
+ externalId?: string | undefined;
65
65
  viewerChunkKey?: string;
66
66
  }, PresencePoseRow[]>;
67
67
  sync: FunctionReference<"mutation", "public", {
@@ -76,7 +76,7 @@ export type ConvexGameApi = {
76
76
  sessionId?: string;
77
77
  kind?: string;
78
78
  label?: string;
79
- externalId?: string;
79
+ externalId?: string | undefined;
80
80
  }, {
81
81
  pose: {
82
82
  x: number;
@@ -98,13 +98,13 @@ export type ConvexGameApi = {
98
98
  messages: FunctionReference<"query", "public", {
99
99
  serverId: string;
100
100
  channelId: string;
101
- externalId?: string;
101
+ externalId?: string | undefined;
102
102
  }, ChatMessage[]>;
103
103
  sendMessage: FunctionReference<"mutation", "public", {
104
104
  serverId: string;
105
105
  channelId: string;
106
106
  body: string;
107
- externalId?: string;
107
+ externalId?: string | undefined;
108
108
  }, ChatSendOutcome>;
109
109
  };
110
110
  leaderboard: {
@@ -104,6 +104,14 @@ export function createConvexPresenceSync(client, api, config, tuning) {
104
104
  const viewerChunks = new Map();
105
105
  const refreshers = new Map();
106
106
  const lastSentAt = new Map();
107
+ // A pose row outlives its tab unless something ends it; the last unsubscribe and an unload both do.
108
+ const leave = (serverId) => {
109
+ void client.mutation(api.presence.leave, { serverId, ...(config.userId === undefined ? {} : { externalId: config.userId }) }).catch(() => undefined);
110
+ };
111
+ const onPageHide = () => {
112
+ for (const serverId of refreshers.keys())
113
+ leave(serverId);
114
+ };
107
115
  return {
108
116
  subscribe(serverId, onChange) {
109
117
  let unsubscribe;
@@ -115,6 +123,8 @@ export function createConvexPresenceSync(client, api, config, tuning) {
115
123
  let listeners = refreshers.get(serverId);
116
124
  if (listeners === undefined) {
117
125
  listeners = new Set();
126
+ if (refreshers.size === 0)
127
+ globalThis.addEventListener?.("pagehide", onPageHide);
118
128
  refreshers.set(serverId, listeners);
119
129
  }
120
130
  listeners.add(refresh);
@@ -127,6 +137,9 @@ export function createConvexPresenceSync(client, api, config, tuning) {
127
137
  viewerChunks.delete(serverId);
128
138
  gates.delete(serverId);
129
139
  lastSentAt.delete(serverId);
140
+ leave(serverId);
141
+ if (refreshers.size === 0)
142
+ globalThis.removeEventListener?.("pagehide", onPageHide);
130
143
  }
131
144
  };
132
145
  },
@@ -39,7 +39,7 @@ export interface HostedWorldOutcome<T> {
39
39
  */
40
40
  export declare function invokeHostedWorld<T>(invocation: HostedWorldInvocation<T>): Promise<HostedWorldOutcome<T>>;
41
41
  /** Default minimum elapsed ms before `tickHostedWorlds` advances a world — override via the factory's `tickMs`. */
42
- export declare const JG_HOSTED_TICK_MS = 1000;
42
+ export declare const JG_HOSTED_TICK_MS = 50;
43
43
  /**
44
44
  * The Convex counterpart of ws's `createWorldGameHost`: serves GameContext worlds statelessly by
45
45
  * reconstructing a hosted session from the persisted `jgHostedWorlds` row on every invocation via
@@ -53,9 +53,9 @@ export declare function createHostedGameServerFunctions(options: {
53
53
  tickMs?: number;
54
54
  }): {
55
55
  getHostedServer: import("convex/server").RegisteredQuery<"public", {
56
- externalId?: string | undefined;
56
+ externalId?: string;
57
57
  gameId: string;
58
- serverId?: string | undefined;
58
+ serverId?: string;
59
59
  }, Promise<{
60
60
  serverId: string;
61
61
  gameId: string;
@@ -64,24 +64,24 @@ export declare function createHostedGameServerFunctions(options: {
64
64
  memberUserIds: string[];
65
65
  } | null>>;
66
66
  joinHostedServer: import("convex/server").RegisteredMutation<"public", {
67
- externalId?: string | undefined;
67
+ externalId?: string;
68
68
  gameId: string;
69
- serverId?: string | undefined;
69
+ serverId?: string;
70
70
  }, Promise<{
71
71
  serverId: string;
72
72
  isNew: boolean;
73
73
  }>>;
74
74
  leaveHostedServer: import("convex/server").RegisteredMutation<"public", {
75
- externalId?: string | undefined;
75
+ externalId?: string;
76
76
  gameId: string;
77
- serverId?: string | undefined;
77
+ serverId?: string;
78
78
  }, Promise<null>>;
79
79
  runHostedCommand: import("convex/server").RegisteredMutation<"public", {
80
80
  command: string;
81
- externalId?: string | undefined;
81
+ externalId?: string;
82
82
  gameId: string;
83
83
  input: any;
84
- serverId?: string | undefined;
84
+ serverId?: string;
85
85
  }, Promise<{
86
86
  ok: false;
87
87
  reason: string;
@@ -89,10 +89,10 @@ export declare function createHostedGameServerFunctions(options: {
89
89
  ok: true;
90
90
  }>>;
91
91
  submitHostedInput: import("convex/server").RegisteredMutation<"public", {
92
- externalId?: string | undefined;
92
+ externalId?: string;
93
93
  frame: any;
94
94
  gameId: string;
95
- serverId?: string | undefined;
95
+ serverId?: string;
96
96
  }, Promise<null>>;
97
97
  tickHostedWorlds: import("convex/server").RegisteredMutation<"internal", {}, Promise<{
98
98
  ticked: number;
@@ -2,6 +2,7 @@ import { defineSchema, internalMutationGeneric, mutationGeneric, queryGeneric, }
2
2
  import { ConvexError, v } from "convex/values";
3
3
  import { createHostedGameRunner, INPUT_COMMAND } from "@jgengine/core/runtime/hostedGameRunner";
4
4
  import { createHostedWorldSessionAsync, } from "@jgengine/core/runtime/hostedWorldSession";
5
+ import { DEFAULT_HOST_TICK_MS, resolveHostTickMs } from "@jgengine/core/runtime/hostPolicy";
5
6
  import { commitIfRevisionMatch } from "./occ.js";
6
7
  import { jgengineHostedTables, resolveActor } from "./server.js";
7
8
  /**
@@ -40,7 +41,7 @@ const query = queryGeneric;
40
41
  const mutation = mutationGeneric;
41
42
  const internalMutation = internalMutationGeneric;
42
43
  /** Default minimum elapsed ms before `tickHostedWorlds` advances a world — override via the factory's `tickMs`. */
43
- export const JG_HOSTED_TICK_MS = 1_000;
44
+ export const JG_HOSTED_TICK_MS = DEFAULT_HOST_TICK_MS;
44
45
  async function getWorldRow(ctx, gameId, serverId) {
45
46
  return await ctx.db
46
47
  .query("jgHostedWorlds")
@@ -100,7 +101,7 @@ async function persistWorldRow(ctx, args) {
100
101
  */
101
102
  export function createHostedGameServerFunctions(options) {
102
103
  const mode = options.auth ?? "anonymous";
103
- const tickMs = options.tickMs ?? JG_HOSTED_TICK_MS;
104
+ const tickMs = resolveHostTickMs(options.tickMs ?? JG_HOSTED_TICK_MS);
104
105
  function resolveGame(gameId) {
105
106
  const game = options.games[gameId];
106
107
  if (game === undefined)
@@ -7,7 +7,7 @@ export declare function randomConvexPlayerId(): string;
7
7
  export declare function resolveConvexMultiplayer(args: {
8
8
  game: GameDefinition;
9
9
  gameId: string;
10
- url?: string;
10
+ url?: string | undefined;
11
11
  client?: ConvexReactClient;
12
12
  api?: ConvexGameApi;
13
13
  userId?: string;