@typetorch/framework 0.3.4 → 0.3.5

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 (50) hide show
  1. package/README.md +97 -9
  2. package/out/analytics/SCHEMA.md +6 -4
  3. package/out/analytics/engine.luau +1 -1
  4. package/out/analytics/schema.d.ts +4 -8
  5. package/out/analytics/schema.luau +1 -7
  6. package/out/analytics/server.d.ts +1 -1
  7. package/out/analytics/server.luau +39 -88
  8. package/out/analytics/settings.d.ts +3 -3
  9. package/out/analytics/settings.luau +3 -3
  10. package/out/detached.d.ts +26 -0
  11. package/out/detached.luau +95 -0
  12. package/out/devtools/admin-server.luau +30 -52
  13. package/out/devtools/client.luau +13 -4
  14. package/out/devtools/health.d.ts +24 -3
  15. package/out/devtools/health.luau +200 -3
  16. package/out/devtools/roll-call.d.ts +11 -6
  17. package/out/devtools/roll-call.luau +11 -6
  18. package/out/index.d.ts +8 -1
  19. package/out/init.luau +6 -0
  20. package/out/kernel.d.ts +186 -2
  21. package/out/kernel.luau +26 -1
  22. package/out/messaging.d.ts +74 -0
  23. package/out/messaging.luau +479 -0
  24. package/out/module.d.ts +9 -0
  25. package/out/module.luau +2 -0
  26. package/out/net/compat.d.ts +147 -0
  27. package/out/net/compat.luau +440 -0
  28. package/out/net/index.d.ts +11 -2
  29. package/out/net/init.luau +26 -3
  30. package/out/net/limits.d.ts +11 -0
  31. package/out/net/limits.luau +25 -0
  32. package/out/net/runtime.d.ts +15 -2
  33. package/out/net/runtime.luau +76 -9
  34. package/out/net/types.d.ts +20 -1
  35. package/out/runtime/dependency.d.ts +79 -0
  36. package/out/runtime/dependency.luau +240 -0
  37. package/out/runtime/player-state.d.ts +30 -0
  38. package/out/runtime/player-state.luau +183 -0
  39. package/out/runtime/relay.d.ts +15 -0
  40. package/out/runtime/relay.luau +58 -0
  41. package/out/runtime/start.luau +119 -40
  42. package/out/servers.d.ts +70 -0
  43. package/out/servers.luau +364 -0
  44. package/out/settings.d.ts +49 -0
  45. package/out/settings.luau +236 -0
  46. package/out/typetorch.d.ts +70 -1
  47. package/out/typetorch.luau +82 -3
  48. package/out/version.d.ts +1 -1
  49. package/out/version.luau +1 -1
  50. package/package.json +1 -1
package/README.md CHANGED
@@ -57,16 +57,25 @@ swap. It needs the TypeTorch kernel in the place (`@typetorch/kernel`).
57
57
 
58
58
  ### Coming from Flamework
59
59
 
60
- Game code only changes imports: `@Service`, `@Controller`, constructor injection, the lifecycle interfaces and
61
- `createNetwork` are the same.
60
+ Game code only changes imports: `@Service`, `@Controller`, constructor injection, `Dependency<T>()`, the lifecycle
61
+ interfaces and `createNetwork` are the same.
62
62
 
63
63
  | Flamework | TypeTorch |
64
64
  |---|---|
65
65
  | `rbxts-transformer-flamework` plugin, `node_modules/@flamework` type root | `@typetorch/transformer` plugin, `node_modules/@typetorch` type root |
66
66
  | `import { Modding, Reflect } from "@flamework/core"` | `import { Modding, Reflect } from "@typetorch/framework"` |
67
+ | `import { Dependency } from "@flamework/core"` | `import { Dependency } from "@typetorch/framework"` (from onInit/onStart on, not in constructors) |
68
+ | `import type` + `Dependency<T>()` to break a cycle | the same, or `Lazy<T>` (below) |
69
+ | `ClientEvents.x.predict(...)` | `network.client.x.emit(...)` |
70
+ | `invokeWithTimeout(timeout, ...)` | `invokeWithTimeout(seconds, ...)` (seconds, 0.5 to 120) |
71
+ | `Observers.observeCharacter` / `observeLocalCharacter` (@rbxts/observers) | still fine: put the stop function in the trove, `this.trove.add(observeCharacter(...))` |
67
72
  | `import { t } from "@rbxts/t"` (still fine) | also `import { t } from "@typetorch/framework"` |
68
73
  | `@metadata flamework:parameters` keys, `"flamework:parameters"` metadata | `@metadata typetorch:parameters`, `"typetorch:parameters"` |
69
74
  | `flamework.build`, `include/flamework`, the `@flamework` Rojo mapping | gone; delete them |
75
+ | `Networking.createEvent` / `createFunction` and their call sites | `createFlameworkCompat` (below) keeps every call site; `createNetwork` is the native API |
76
+
77
+ `typetorch migrate --from flamework` (CLI) does most of this, and the docs' `guides/from-flamework.md` has the whole
78
+ mapping.
70
79
 
71
80
  ## Usage
72
81
 
@@ -99,6 +108,19 @@ export class CoinService extends Module implements OnStart {
99
108
  - **Modules:** `@Service()` (server) and `@Controller()` (client) classes extending `Module`. Constructor parameters
100
109
  are other modules, injected by type. Lifecycle: `OnInit` (sequential, dependencies first), `OnStart` (spawned),
101
110
  `OnStop` (reverse order), `OnTick`, `OnPhysics`, `OnRender`, `OnPlayerAdded` (replays players already in the server).
111
+ - **Modules outside the constructor:** `Dependency<T>()` (or `TypeTorch.module<T>()`) returns the running module of
112
+ type T from anywhere: a method, a plain class a module built, a command handler. `TypeTorch.tryModule<T>()` returns
113
+ undefined instead of throwing. They work once every module is constructed (from onInit on); in a constructor, a
114
+ field initializer or module top-level code they throw `"ShopService isn't constructed yet: ..."`. Only this realm's
115
+ modules, and per generation: after a swap they return the new generation's instance. An `import type` of the class is
116
+ enough (no require cycle).
117
+ - **Cycles:** two modules that need each other take one side as `Lazy<T>`, resolved on first use:
118
+ `constructor(private readonly team: Lazy<TeamService>)` (needs `@typetorch/transformer` 0.2.1+) or a field,
119
+ `private readonly team = Lazy<TeamService>()` (any transformer). `this.team.get()` works from onInit on (in a
120
+ constructor it throws), and the start order ignores lazy edges. A cycle error names the cycle and suggests this.
121
+ - **Per-player state:** `this.ctx.playerState("cooldowns.v1", (player) => init)` (or `TypeTorch.playerState`) returns
122
+ `{ get, set, has, delete }` keyed by UserId. It survives swaps (persist), and a player's entry goes when they really
123
+ leave, never on a swap. The dev menu shows it under Modules > State > persist, `__playerState`.
102
124
  - **Bad deploys roll back (kernel 0.3.2):** a server `onStart` that throws is reported to the kernel, which rolls the
103
125
  server back to its last known good artifact when it happens within 30 s of start (so do 3 errors from the new
104
126
  code's scripts in that time). On a real shutdown every module's `onStop` runs too (the kernel's BindToClose).
@@ -106,6 +128,18 @@ export class CoinService extends Module implements OnStart {
106
128
  behind. `TypeTorch.persist(key, init)` (or `this.ctx.persist`) keeps plain data across swaps.
107
129
  - **Network:** `createNetwork<C2S, S2C>()` with nested namespaces. The server checks rate limits, shape limits and the
108
130
  generated type guard on every client message. `setNetworkLimits({ "chat.say": { maxString: 200 } })` tunes a leaf.
131
+ Requests wait 15 s for the answer: `invokeWithTimeout(5, ...args)` sets it per call, and a leaf's `timeout`
132
+ (`setNetworkLimits({ "vc.spawn": { timeout: 30 } })`, read by the client, so set it in shared code) per leaf; both in
133
+ seconds, 0.5 to 120. `network.client.x.emit(...args)` runs this client's own `on` handlers for a server -> client
134
+ leaf right away, with no traffic (Flamework's `predict`); `network.server.x.emit(player, ...args)` does the same on
135
+ the server, for tests. A request handler may return a Promise: the answer waits for it.
136
+ - **Flamework networking names:** `createFlameworkCompat<C2SEvents, S2CEvents, C2SFunctions, S2CFunctions>()` returns
137
+ `{ ServerEvents, ClientEvents, ServerFunctions, ClientFunctions, GlobalEvents, GlobalFunctions }` with
138
+ `@flamework/networking` 1.x's methods (`connect` returning a connection, `fire(player | players)`, `broadcast`,
139
+ `except`, `predict`, `setCallback`, `invoke`, `invokeWithTimeout`, calling a leaf directly) on this same network:
140
+ same guards, limits and swap rules, so a Flamework game's call sites compile unchanged. Listeners run on their own
141
+ thread each, like Flamework's, and belong to the running generation (no trove needed). Not carried over: server ->
142
+ client requests, middleware, `Networking.Unreliable`. `typetorch build` notes a game still using it.
109
143
  Across a swap (0.3.0): what the server sends a player waits until that player's client runs the new generation
110
144
  (reliable messages are queued, unreliable ones dropped), and on kernel 0.3.2 a request the server can no longer
111
145
  answer fails at once ("The game is updating, try again.") instead of timing out.
@@ -182,6 +216,13 @@ this.trove.add(TypeTorch.onUpdatePending((update) => (hint.Visible = !update.can
182
216
 
183
217
  // State that survives swaps (plain data only)
184
218
  const scores = TypeTorch.persist("scores.v1", () => new Map<number, number>());
219
+ // Per player, removed when the player really leaves (same as this.ctx.playerState)
220
+ const cooldowns = TypeTorch.playerState("cooldowns.v1", () => ({ lastUse: 0 }));
221
+ cooldowns.get(player).lastUse = os.clock();
222
+
223
+ // The running module of a type (same as Dependency<T>()), from onInit/onStart on
224
+ TypeTorch.module<ShopService>().open(player);
225
+ TypeTorch.tryModule<ShopService>()?.open(player); // undefined instead of an error
185
226
 
186
227
  // Dev and roles (server: the kernel decides; client: only the local player, cosmetic)
187
228
  if (TypeTorch.isOwner(player)) showOwnerPanel(player);
@@ -198,6 +239,23 @@ TypeTorch.onLog((entry) => errors.push(entry)); // don't print from inside it
198
239
 
199
240
  // Hot assets (below): same as hotAsset(...)
200
241
  const shop = TypeTorch.asset("ui/shop");
242
+
243
+ // Live values, server only (kernel 0.3.8): the signed settings record's `game` field (`typetorch settings set game.<key> <json>`).
244
+ const price = TypeTorch.liveConfig("shop.price", { default: 50, parse: (raw) => (typeIs(raw, "number") ? raw : 50) });
245
+ price.get();
246
+ this.trove.add(price.onChanged((value) => this.reprice(value)));
247
+
248
+ // Cross-server, server only. Kernel 0.3.8: game topics over ONE kernel-held MessagingService topic; listeners hear
249
+ // prod servers and this branch by default (dev branches never reach prod); publish queues and retries, max 1 KiB.
250
+ this.trove.add(TypeTorch.messaging.subscribe<Ban>("1guard", (ban, meta) => applyBan(ban, meta.jobId)));
251
+ TypeTorch.messaging.publish("1guard", { kind: "ban", userId }, { to: "prod" });
252
+
253
+ // The universe's live servers (the roll call; cached per server, yields up to ~3 s when stale) and this server's public fields
254
+ TypeTorch.setServerInfo({ region: "eu", vc: true }); // JSON, at most 400 bytes, kept across swaps
255
+ const list = TypeTorch.servers(); // [{ jobId, placeVersion, players, maxPlayers, serverType, branch, channel, uptime, here, info }]
256
+
257
+ // A library call a swap must not cut off halfway (kernel 0.3.8): runs on a kernel thread; write what must survive into persist.
258
+ TypeTorch.runDetached(() => store.StartSessionAsync(`Player_${userId}`)).then((profile) => attach(player, profile));
201
259
  ```
202
260
 
203
261
  - **`startInfo.reason`:** `boot`, `deploy`, `rollback`, `branch`, `pin`, `reload`, `server_rollback`,
@@ -214,6 +272,25 @@ const shop = TypeTorch.asset("ui/shop");
214
272
  reason and timings, the next artifact in `onSwapOut`, `onUpdatePending`, server-side `onPlayerDevChanged` and
215
273
  `requestReload`. On older kernels `startInfo` still knows boot vs swap, the previous artifact and branch (the
216
274
  framework records them), `onBranchChanged` still fires, and the reason of a swap is `unknown`.
275
+ - **`messaging`** (kernel 0.3.8): `subscribe(topic, (data, meta) => ...)` returns a disconnect; `meta` = `{ channel,
276
+ branch, jobId, serverType, placeVersion, sentAt, self, replayed? }`; `{ from: "all" | "prod" | "branch" }` changes
277
+ who it hears. `publish(topic, data, { to })` throws on a bad topic, non-JSON data or a message over 1 KiB (with its
278
+ size), returns false when dropped at once (queue full). Studio loops back; the cloud test publishes nothing; older
279
+ kernels throw "needs kernel 0.3.8". `servers()` works on every kernel (0.3.8 holds the roll call topic, so a swap
280
+ doesn't subscribe again).
281
+ - **`liveConfig(key, { default, parse? })`** (kernel 0.3.8, plans/20): reads `game[key]` of the signed settings record the
282
+ kernel verified (signed by both prod keys, so game code can't forge it); `get()` and `onChanged(fn)` (only on real
283
+ changes; generation-scoped). The default without a record or key, when `parse` throws (warned per value), and on older
284
+ kernels (warned once: "needs kernel 0.3.8"; no ConfigService fallback). `TypeTorch.settings()` is the whole copy
285
+ (`seq`, `at`, `game`, `analytics`, `fleet`, ...): server only, it holds tokens.
286
+ - **`runDetached(fn)`** (kernel 0.3.8, server only): runs `fn` on a thread the kernel owns, so a deploy's hard stop
287
+ (which kills this generation's threads mid-call) can't cut it off: for ProfileStore / ProfileService loads, saves and
288
+ releases. Returns a Promise that settles while this generation runs; after a swap the result is dropped, so a job
289
+ whose result must survive writes it into `persist` itself. The job keeps this generation's closures alive until it
290
+ ends. Errors reject it and go to the kernel's log with the generation's name (never the health window); at most 256
291
+ run at once per server (then it throws); one past 60 s is logged and flagged in Server > Status. Older kernels
292
+ throw "needs kernel 0.3.8; use the DataHost job queue (Player data guide)": the other option, which works on every
293
+ kernel. `features.runDetached` tells which one this server has.
217
294
  - **Edit mode** (UI Labs stories, no kernel): `running` is false, identity has defaults, `persist` keeps a local table,
218
295
  events never fire, and the server-only reads throw.
219
296
 
@@ -227,6 +304,8 @@ pieces that keep it safe across swaps:
227
304
  - **Session handles live in `persist`**, keyed by `UserId`. `onPlayerAdded` replays everyone after a swap, so it
228
305
  re-attaches to the open session instead of loading again.
229
306
  - **Release only on `Players.PlayerRemoving`**, never in `onStop` or `onSwapOut`.
307
+ - **Library calls can't be cut off by a swap:** run them as DataHost jobs (any kernel) or with
308
+ `TypeTorch.runDetached` (kernel 0.3.8).
230
309
  - **Split store names by channel** (`TypeTorch.channel === "prod" ? "PlayerData" : "PlayerData_dev"`).
231
310
 
232
311
  Full example with ProfileStore: [Player data guide](https://github.com/typetorch/docs/blob/main/guides/player-data.md).
@@ -287,7 +366,7 @@ self-hosted DuckDB analytics server). Nothing runs until an engine is created: n
287
366
  ```ts
288
367
  import { AnalyticsEngine } from "@typetorch/framework";
289
368
 
290
- // A server module (onInit): reads the ConfigService key TypeTorchAnalytics.
369
+ // A server module (onInit): the sink comes from the signed settings' `analytics` (kernel 0.3.8).
291
370
  const analytics = new AnalyticsEngine();
292
371
  analytics.track(player, "quest_done", { quest: "tutorial" }); // custom
293
372
  analytics.step(player, "onboarding", 3, "opened_shop"); // funnels
@@ -317,13 +396,13 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
317
396
  `TTZone`, named by a `Name` attribute or the instance name), `screens` (ScreenGuis in PlayerGui, GuiObjects tagged
318
397
  `TTScreen`), `recording` (the first-ever session in detail), `fleet` (kernel 0.3.2 heartbeats and deploy reports),
319
398
  `identity` (one `{ pid, uid }` row per session to the dev's own server, below). `settings` (server only) replaces the
320
- ConfigService key, for tests.
399
+ signed settings' `analytics`, for tests, Studio and kernels before 0.3.8.
321
400
  - **Every row** carries the time (server clock), a random player id (never the UserId), the session, the server
322
401
  (JobId, type, place), the artifact (id, seq, branch, channel), the device, new vs returning, the player's state
323
402
  (`zone:Lobby|screen:Shop|activity:round`) and experiment variants. The exact rows: `src/analytics/SCHEMA.md`.
324
403
  - **Experiments:** `experiment(...)` is deterministic per player and name (they keep their variant in every session),
325
404
  may yield briefly the first time (until the player's id loads), and stamps the variant on the player's later events.
326
- The settings key turns one off (`active: false`: everyone gets the first variant), sets `weights` or forces a
405
+ The settings turn one off (`active: false`: everyone gets the first variant), sets `weights` or forces a
327
406
  `variant`, live.
328
407
  - **First-ever session:** for new players (a share of them, `recordShare`), the client records character and camera
329
408
  about 10 times a second, every input (never while a TextBox or the chat has focus; text boxes only say which box was
@@ -333,7 +412,7 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
333
412
  row carries a UserId.
334
413
  - **Identities** (option `identity`, default on): once the pid is known, one row `{ pid, uid, t }` (the UserId, nothing
335
414
  else) per session goes to the dev's own server only, never into the events: duckdb games in the batch body, Basin
336
- games to the fleet API (`TypeTorchFleet` url + `/v1/identity`, or the `identity` / `identityToken` settings). The
415
+ games to the fleet API (the settings' `fleet` url + `/v1/identity`, or the `identity` / `identityToken` settings). The
337
416
  analytics server keeps pid -> UserId in a deletable table (support lookups, Right to Erasure).
338
417
  - **Player ids:** DataStore `TypeTorchAnalytics`, key `p/<UserId>` -> `{ pid, first, last }`: one read per join, a
339
418
  write on the first join and at leave. Deleting the key (Right to Erasure) leaves that player's rows anonymous.
@@ -352,9 +431,10 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
352
431
  - **Friends in the server** (the join's `friends`): one `Players:GetFriendsAsync` per join (at most 10 pages, none
353
432
  when the player is alone) intersected with the players there, not a web call per player.
354
433
 
355
- **Settings** (server only, never sent to clients): the ConfigService key `TypeTorchAnalytics`, written with
356
- `writeSettings()` from `@typetorch/analytics` (Open Cloud, universe:write) or in Creator Hub (Configs); no CLI command
357
- writes it. Re-read live every few minutes:
434
+ **Settings** (server only, never sent to clients): the signed settings record's `analytics` field (kernel 0.3.8,
435
+ plans/20), written with `typetorch settings set analytics -` (JSON on stdin; `@typetorch/analytics`' `writeSettings()`
436
+ and `bun run local` call it). Every new copy applies live within seconds (the CLI pings servers). Before kernel 0.3.8
437
+ there are no settings unless the game passes `new AnalyticsEngine({ settings })`:
358
438
 
359
439
  ```json
360
440
  { "backend": "basin", "events": "https://<stream-id>.ingest.cloudflare.com",
@@ -392,6 +472,14 @@ bun run build # rbxtsc --type package -> out/
392
472
  package.json.
393
473
  `scripts/test-layout.luau` checks the dev menu's pane layout core (`devtools/layout.ts`: splits and their limits,
394
474
  closing, pop out and dock, singleton pages, ratios, focus, and cleaning a remembered layout).
475
+ `scripts/test-modules.luau` runs the real `startServer`/`startClient` of `out/` (with RuntimeLib, Promise, trove and
476
+ t from node_modules) over several generations that share a persist store: `Dependency<T>()`, `TypeTorch.module` /
477
+ `tryModule` and their errors, `Lazy<T>` (parameters and fields, cycles, start order), network `emit` and request
478
+ timeouts and `playerState` (swaps, leaves).
479
+ `scripts/test-flamework-compat.luau` (same harness) drives `createFlameworkCompat` through each generation's
480
+ dispatcher: connect (one thread per call, guards, Disconnect, troves), fire / except / broadcast / the call
481
+ shorthand, setCallback with values and Promises, predict, a swap, and the client side (invoke, invokeWithTimeout,
482
+ `createClient({ defaultTimeout })`).
395
483
  `scripts/test-analytics.luau` checks the analytics engine's pure parts (experiment assignment, settings, the queue
396
484
  and HTTP budget, tt-rec-1, the sink request bodies); `test-analytics-server.luau` and `test-analytics-client.luau`
397
485
  run the compiled server and client cores against mocked services (sessions, intake, retries, a swap with a request
@@ -128,8 +128,10 @@ start, i.e. the row's `t`). Time is the running sum of `dt`.
128
128
 
129
129
  ## Sink settings
130
130
 
131
- ConfigService key `TypeTorchAnalytics` (server-only), a JSON object (or a JSON string of one). Re-read on
132
- `UpdateAvailable` and every 3 minutes, so dials change live. `new AnalyticsEngine({ settings })` replaces the key.
131
+ The signed settings record's `analytics` field (kernel 0.3.8, plans/20; server-only; before 0.3.8 the ConfigService key
132
+ `TypeTorchAnalytics`), a JSON object, written with `typetorch settings set analytics -` (JSON on stdin; the analytics
133
+ repo's `bun run local` does it). Every new signed copy applies at once, so dials change live (the CLI pings servers).
134
+ `new AnalyticsEngine({ settings })` replaces it (tests, Studio, kernels before 0.3.8).
133
135
 
134
136
  ```json
135
137
  {
@@ -154,8 +156,8 @@ ConfigService key `TypeTorchAnalytics` (server-only), a JSON object (or a JSON s
154
156
  | `recordShare` | 1 | 0..1, share of new players recorded (stable per pid) |
155
157
  | `techEvery` | 60 | 15..3600 seconds between tech samples |
156
158
  | `experiments` | none | Per experiment: `active: false` (everyone gets the first variant, not stamped), `weights` (per variant, in the game's order), `variant` (force one) |
157
- | `identity` | `TypeTorchFleet` url + `/v1/identity` | basin: where identity rows go (below); none and no readable `TypeTorchFleet`: not sent. duckdb: unused |
158
- | `identityToken` | the `TypeTorchFleet` token | basin: the token for `identity`. Never logged |
159
+ | `identity` | the settings' `fleet` url + `/v1/identity` | basin: where identity rows go (below); none and no `fleet` in the settings: not sent. duckdb: unused |
160
+ | `identityToken` | the settings' `fleet` token | basin: the token for `identity`. Never logged |
159
161
 
160
162
  Unknown fields are ignored. Settings errors are warned once (never with the token).
161
163
 
@@ -10,7 +10,7 @@ local ServerAnalytics = TS.import(script, script.Parent, "server").ServerAnalyti
10
10
  * client. Nothing runs until one is created. Every engine created in a generation shares one core, which stops with
11
11
  * the generation; its unsent rows survive a hot swap in `persist`.
12
12
  *
13
- * The server sends rows to the sink in the settings (ConfigService key `TypeTorchAnalytics`, or `options.settings`).
13
+ * The server sends rows to the sink in the settings (the signed settings' `analytics`, kernel 0.3.8; or `options.settings`).
14
14
  * The client sends everything through the server, never to the internet. See the README "Analytics" section and
15
15
  * src/analytics/SCHEMA.md.
16
16
 
@@ -5,10 +5,6 @@
5
5
  */
6
6
  /** Row format version (column `v`). */
7
7
  export declare const SCHEMA_VERSION = 1;
8
- /** The ConfigService key (server-only) that holds the sink settings. */
9
- export declare const SETTINGS_KEY = "TypeTorchAnalytics";
10
- /** The kernel's fleet API settings `{ url, token }` (server-only ConfigService key); identities can go there. */
11
- export declare const FLEET_SETTINGS_KEY = "TypeTorchFleet";
12
8
  /** The first-session recording codec (column `codec`). */
13
9
  export declare const RECORDING_CODEC = "tt-rec-1";
14
10
  /** Most bytes of the `props` JSON text. Larger props become `{"_trunc":<bytes>}`. */
@@ -91,7 +87,7 @@ export interface ExperimentOverride {
91
87
  /** Everyone gets this variant (must be one of the game's variants). */
92
88
  variant?: string;
93
89
  }
94
- /** The sink settings: ConfigService key `TypeTorchAnalytics` (server-only), or `new AnalyticsEngine({ settings })`. */
90
+ /** The sink settings: the signed settings' `analytics` (kernel 0.3.8, server-only), or `new AnalyticsEngine({ settings })`. */
95
91
  export interface AnalyticsSettings {
96
92
  backend: "basin" | "duckdb";
97
93
  /** basin: the events stream's ingest URL; duckdb: the analytics server's ingest URL. */
@@ -109,12 +105,12 @@ export interface AnalyticsSettings {
109
105
  /** Live experiment overrides by experiment name. */
110
106
  experiments?: Record<string, ExperimentOverride>;
111
107
  /**
112
- * basin: where identity rows go (the fleet API's `POST /v1/identity` URL). Default: the `TypeTorchFleet` key's url
108
+ * basin: where identity rows go (the fleet API's `POST /v1/identity` URL). Default: the settings' `fleet` url
113
109
  * + /v1/identity when the server can read it; without either, identities aren't sent. duckdb: unused (they go in
114
110
  * the batch body).
115
111
  */
116
112
  identity?: string;
117
- /** basin: the token for `identity` (default: the `TypeTorchFleet` token). Never logged. */
113
+ /** basin: the token for `identity` (default: the settings' `fleet` token). Never logged. */
118
114
  identityToken?: string;
119
115
  }
120
116
  /** AnalyticsSettings with every default filled in. */
@@ -147,7 +143,7 @@ export interface AnalyticsPurchase {
147
143
  }
148
144
  /** What the automatic collectors do. Everything is on by default. */
149
145
  export interface AnalyticsOptions {
150
- /** Server only: the sink settings. When set, the ConfigService key isn't read (tests, Studio). */
146
+ /** Server only: the sink settings. When set, the signed settings aren't read (tests, Studio, kernels before 0.3.8). */
151
147
  settings?: AnalyticsSettings;
152
148
  /** Server: joins, leaves, device, join source, first-ever vs returning. Default true. */
153
149
  sessions?: boolean;
@@ -8,10 +8,6 @@
8
8
  ]]
9
9
  --* Row format version (column `v`).
10
10
  local SCHEMA_VERSION = 1
11
- --* The ConfigService key (server-only) that holds the sink settings.
12
- local SETTINGS_KEY = "TypeTorchAnalytics"
13
- --* The kernel's fleet API settings `{ url, token }` (server-only ConfigService key); identities can go there.
14
- local FLEET_SETTINGS_KEY = "TypeTorchFleet"
15
11
  --* The first-session recording codec (column `codec`).
16
12
  local RECORDING_CODEC = "tt-rec-1"
17
13
  --* Most bytes of the `props` JSON text. Larger props become `{"_trunc":<bytes>}`.
@@ -26,7 +22,7 @@ local PROPS_MAX_BYTES = 4096
26
22
 
27
23
  ]]
28
24
  --* Live overrides of one per-player experiment (settings key `experiments`).
29
- --* The sink settings: ConfigService key `TypeTorchAnalytics` (server-only), or `new AnalyticsEngine({ settings })`.
25
+ --* The sink settings: the signed settings' `analytics` (kernel 0.3.8, server-only), or `new AnalyticsEngine({ settings })`.
30
26
  --* AnalyticsSettings with every default filled in.
31
27
  --* Event properties: a flat JSON object (nested tables work but are harder to query).
32
28
  --* What the automatic collectors do. Everything is on by default.
@@ -35,8 +31,6 @@ local PROPS_MAX_BYTES = 4096
35
31
  --* Counters of the server engine (`AnalyticsEngine.stats()`).
36
32
  return {
37
33
  SCHEMA_VERSION = SCHEMA_VERSION,
38
- SETTINGS_KEY = SETTINGS_KEY,
39
- FLEET_SETTINGS_KEY = FLEET_SETTINGS_KEY,
40
34
  RECORDING_CODEC = RECORDING_CODEC,
41
35
  PROPS_MAX_BYTES = PROPS_MAX_BYTES,
42
36
  }
@@ -63,7 +63,7 @@ export declare class ServerAnalytics {
63
63
  private settingsErrors;
64
64
  /** The first settings read finished (found or not), so experiment overrides are as known as they get. */
65
65
  private settingsTried;
66
- /** The `TypeTorchFleet` value (Basin games send identities to the fleet API). */
66
+ /** The settings' `fleet` value (Basin games send identities to the fleet API). */
67
67
  private fleetSettings?;
68
68
  private nextFlushAt;
69
69
  private backoffUntil;
@@ -2,7 +2,6 @@
2
2
  local TS = _G[script]
3
3
  local _services = TS.import(script, TS.getModule(script, "@rbxts", "services"))
4
4
  local CollectionService = _services.CollectionService
5
- local ConfigService = _services.ConfigService
6
5
  local DataStoreService = _services.DataStoreService
7
6
  local HttpService = _services.HttpService
8
7
  local LocalizationService = _services.LocalizationService
@@ -12,6 +11,10 @@ local Stats = _services.Stats
12
11
  local Workspace = _services.Workspace
13
12
  local maybeServerDispatcher = TS.import(script, script.Parent.Parent, "net", "runtime").maybeServerDispatcher
14
13
  local observePlayers = TS.import(script, script.Parent.Parent, "players").observePlayers
14
+ local _settings = TS.import(script, script.Parent.Parent, "settings")
15
+ local currentSettings = _settings.currentSettings
16
+ local onSettings = _settings.onSettings
17
+ local settingsSupported = _settings.settingsSupported
15
18
  local _typetorch = TS.import(script, script.Parent.Parent, "typetorch")
16
19
  local closeHooksSupported = _typetorch.closeHooksSupported
17
20
  local onGenerationClose = _typetorch.onGenerationClose
@@ -39,15 +42,13 @@ local newQueueState = _queue.newQueueState
39
42
  local RowQueue = _queue.RowQueue
40
43
  local takeToken = _queue.takeToken
41
44
  local _schema = TS.import(script, script.Parent, "schema")
42
- local FLEET_SETTINGS_KEY = _schema.FLEET_SETTINGS_KEY
43
45
  local RECORDING_CODEC = _schema.RECORDING_CODEC
44
46
  local SCHEMA_VERSION = _schema.SCHEMA_VERSION
45
- local SETTINGS_KEY = _schema.SETTINGS_KEY
46
- local _settings = TS.import(script, script.Parent, "settings")
47
- local identityTarget = _settings.identityTarget
48
- local parseSettings = _settings.parseSettings
49
- local recordsSupported = _settings.recordsSupported
50
- local DEFAULT_TECH_EVERY = _settings.DEFAULT_TECH_EVERY
47
+ local _settings_1 = TS.import(script, script.Parent, "settings")
48
+ local identityTarget = _settings_1.identityTarget
49
+ local parseSettings = _settings_1.parseSettings
50
+ local recordsSupported = _settings_1.recordsSupported
51
+ local DEFAULT_TECH_EVERY = _settings_1.DEFAULT_TECH_EVERY
51
52
  local _sinks = TS.import(script, script.Parent, "sinks")
52
53
  local buildRequests = _sinks.buildRequests
53
54
  local estimateEventBytes = _sinks.estimateEventBytes
@@ -104,7 +105,6 @@ local RECORDING_MAX_CHUNKS = 80
104
105
  local RECORDING_MAX_SECONDS = 900
105
106
  --* The join event waits this long for the client's device info.
106
107
  local DEVICE_WAIT = 8
107
- local SETTINGS_REFRESH = 180
108
108
  local ZONE_INTERVAL = 0.5
109
109
  local ZONE_TAG = "TTZone"
110
110
  --* One player's session. Plain data (persist), keyed by tostring(UserId).
@@ -484,7 +484,7 @@ do
484
484
  end
485
485
  function ServerAnalytics:track(player, kind, name, props)
486
486
  if not isEventName(name) then
487
- warn("[src/analytics/server.ts:458]", `[analytics] {kind} event name must be 1-64 characters: {string.sub(tostring(name), 1, 80)}`)
487
+ warn("[src/analytics/server.ts:455]", `[analytics] {kind} event name must be 1-64 characters: {string.sub(tostring(name), 1, 80)}`)
488
488
  return nil
489
489
  end
490
490
  local session = if player then self:sessionOf(player) else nil
@@ -592,7 +592,7 @@ do
592
592
  end
593
593
  local control = _condition
594
594
  if not isExperimentName(name) or not isVariantList(variants) then
595
- warn("[src/analytics/server.ts:509]", `[analytics] experiment("{string.sub(tostring(name), 1, 64)}"): bad name or variants (1-16 distinct names)`)
595
+ warn("[src/analytics/server.ts:506]", `[analytics] experiment("{string.sub(tostring(name), 1, 64)}"): bad name or variants (1-16 distinct names)`)
596
596
  return control
597
597
  end
598
598
  local session = self:sessionOf(player)
@@ -657,7 +657,7 @@ do
657
657
  function ServerAnalytics:applySettings(parsed)
658
658
  local errorsText = table.concat(parsed.errors, "; ")
659
659
  if errorsText ~= table.concat(self.settingsErrors, "; ") and errorsText ~= "" then
660
- warn("[src/analytics/server.ts:544]", `[analytics] {SETTINGS_KEY}: {errorsText}`)
660
+ warn("[src/analytics/server.ts:541]", `[analytics] settings.analytics: {errorsText}`)
661
661
  end
662
662
  self.settingsErrors = parsed.errors
663
663
  local hadSettings = self.settings ~= nil
@@ -682,91 +682,42 @@ do
682
682
  self.settingsTried = true
683
683
  return nil
684
684
  end
685
+ -- Kernel 0.3.8 (plans/20): the signed settings record's `analytics` (the sink) and `fleet` (identities on Basin),
686
+ -- live through the kernel's change hook. Older kernels have no settings (no ConfigService fallback): pass
687
+ -- `new AnalyticsEngine({ settings })`.
688
+ if not settingsSupported() then
689
+ if self.kernel ~= nil and self.kernel.test ~= true then
690
+ warn("[src/analytics/server.ts:568]", `[analytics] no sink settings: they come from the signed settings record (kernel 0.3.8+, "typetorch settings set analytics -"); this server runs kernel {self.kernel.kernelVersion}. Or pass new AnalyticsEngine(\{ settings \})`)
691
+ end
692
+ self.settingsTried = true
693
+ return nil
694
+ end
685
695
  local lastRaw
686
- local missing = false
687
- local snapshot
688
- local read = function(force)
689
- if not snapshot or (missing and not force) then
690
- return nil
696
+ local apply = function(settings)
697
+ local _result = settings
698
+ if _result ~= nil then
699
+ _result = _result.fleet
691
700
  end
692
- local fleetOk, fleetValue = pcall(function()
693
- return snapshot:GetValue(FLEET_SETTINGS_KEY)
694
- end)
695
- if fleetOk then
696
- self.fleetSettings = fleetValue
701
+ self.fleetSettings = _result
702
+ local _value = settings
703
+ if _value ~= nil then
704
+ _value = _value.analytics
697
705
  end
698
- local ok, value = pcall(function()
699
- return snapshot:GetValue(SETTINGS_KEY)
700
- end)
701
- if not ok then
702
- return nil
703
- end
704
- missing = value == nil
706
+ local value = _value
705
707
  -- Compare without decoding twice; the text never reaches a log.
706
708
  local encodedOk, raw = pcall(function()
707
- return if type(value) == "string" then value else HttpService:JSONEncode(value)
709
+ return if value == nil then "" else HttpService:JSONEncode(value)
708
710
  end)
709
- local text = if value == nil then "" elseif encodedOk then raw else tostring(os.clock())
711
+ local text = if encodedOk then raw else tostring(os.clock())
710
712
  if text == lastRaw then
711
713
  return nil
712
714
  end
713
715
  lastRaw = text
714
716
  self:applySettings(parseSettings(value, decode))
715
717
  end
716
- self.trove:add(task.spawn(function()
717
- local delay = 2
718
- do
719
- local attempt = 1
720
- local _shouldIncrement = false
721
- while true do
722
- if _shouldIncrement then
723
- attempt += 1
724
- else
725
- _shouldIncrement = true
726
- end
727
- if not (attempt <= 6 and not snapshot) then
728
- break
729
- end
730
- local ok, result = pcall(function()
731
- return ConfigService:GetConfigAsync()
732
- end)
733
- if ok then
734
- snapshot = result
735
- else
736
- if attempt == 1 then
737
- warn("[src/analytics/server.ts:590]", `[analytics] ConfigService unavailable: {string.sub(tostring(result), 1, 120)}`)
738
- end
739
- task.wait(delay)
740
- delay = math.min(delay * 2, 30)
741
- end
742
- end
743
- end
744
- if not snapshot then
745
- self.settingsTried = true
746
- return nil
747
- end
748
- local current = snapshot
749
- self.trove:connect(current.UpdateAvailable, function()
750
- pcall(function()
751
- return current:Refresh()
752
- end)
753
- read(true)
754
- end)
755
- pcall(function()
756
- return self.trove:connect(current:GetValueChangedSignal(SETTINGS_KEY), function()
757
- return read(true)
758
- end)
759
- end)
760
- read(true)
761
- self.settingsTried = true
762
- while true do
763
- task.wait(SETTINGS_REFRESH)
764
- pcall(function()
765
- return current:Refresh()
766
- end)
767
- read(false)
768
- end
769
- end))
718
+ apply(currentSettings())
719
+ self.settingsTried = true
720
+ self.trove:add(onSettings(apply))
770
721
  end
771
722
  function ServerAnalytics:dataStore()
772
723
  local ok, store = pcall(function()
@@ -1481,7 +1432,7 @@ do
1481
1432
  end
1482
1433
  end)
1483
1434
  if not ok then
1484
- warn("[src/analytics/server.ts:1058]", `[analytics] client message failed: {string.sub(tostring(err), 1, 200)}`)
1435
+ warn("[src/analytics/server.ts:1033]", `[analytics] client message failed: {string.sub(tostring(err), 1, 200)}`)
1485
1436
  end
1486
1437
  end)
1487
1438
  self.trove:add(function()
@@ -1761,7 +1712,7 @@ do
1761
1712
  return nil
1762
1713
  end
1763
1714
  self.lastWarnAt = now
1764
- warn("[src/analytics/server.ts:1247]", `[analytics] {message}`)
1715
+ warn("[src/analytics/server.ts:1222]", `[analytics] {message}`)
1765
1716
  end
1766
1717
  function ServerAnalytics:post(request)
1767
1718
  local ok, response = pcall(function()
@@ -1883,7 +1834,7 @@ do
1883
1834
  end
1884
1835
  local reason = _result
1885
1836
  local fix = if status == 530 then ": the tunnel is down, run bun run local on the dev PC" else ""
1886
- self:warn(if outcome == "config" then `{what} got HTTP {status}: check the URL and token in {SETTINGS_KEY}; retrying in {math.floor(wait)} s` else `{what} failed ({reason}{fix}); retrying in {math.floor(wait)} s`)
1837
+ self:warn(if outcome == "config" then `{what} got HTTP {status}: check the URL and token in settings.analytics (typetorch settings set analytics -); retrying in {math.floor(wait)} s` else `{what} failed ({reason}{fix}); retrying in {math.floor(wait)} s`)
1887
1838
  end
1888
1839
  end
1889
1840
  -- Recordings without a request (basin without a recordings URL) were never taken; nothing else is left.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The sink settings (ConfigService key `TypeTorchAnalytics`, or `new AnalyticsEngine({ settings })`): parsing with
2
+ * The sink settings (the signed settings' `analytics`, kernel 0.3.8, or `new AnalyticsEngine({ settings })`): parsing with
3
3
  * defaults and clamps. Errors never repeat the token.
4
4
  *
5
5
  * Pure: no imports and no services, so it also runs offline under Lune (scripts/test-analytics.luau). The JSON
@@ -18,14 +18,14 @@ export interface ParsedSettings {
18
18
  * no settings and no errors. Unknown fields are ignored.
19
19
  */
20
20
  export declare function parseSettings(raw: unknown, decode: (text: string) => unknown): ParsedSettings;
21
- /** Where identity rows go on a Basin game: the fleet API (`TypeTorchFleet` = { url, token }) at /v1/identity. */
21
+ /** Where identity rows go on a Basin game: the fleet API (the settings' `fleet` = { url, token }) at /v1/identity. */
22
22
  export interface IdentityTarget {
23
23
  url: string;
24
24
  token?: string;
25
25
  }
26
26
  /**
27
27
  * The identity target for these settings: duckdb none (identities ride in the batch body); basin the `identity` URL,
28
- * else the fleet API's /v1/identity from the `TypeTorchFleet` value (a table or JSON `{ url, token }`), else none.
28
+ * else the fleet API's /v1/identity from the settings' `fleet` value (a table or JSON `{ url, token }`), else none.
29
29
  */
30
30
  export declare function identityTarget(settings: ResolvedSettings, fleetRaw: unknown, decode: (text: string) => unknown): IdentityTarget | undefined;
31
31
  /** Whether recordings can be sent at all: duckdb always (one body), basin only with a recordings stream URL. */
@@ -1,7 +1,7 @@
1
1
  -- Compiled with roblox-ts v3.0.0
2
2
  --[[
3
3
  *
4
- * The sink settings (ConfigService key `TypeTorchAnalytics`, or `new AnalyticsEngine({ settings })`): parsing with
4
+ * The sink settings (the signed settings' `analytics`, kernel 0.3.8, or `new AnalyticsEngine({ settings })`): parsing with
5
5
  * defaults and clamps. Errors never repeat the token.
6
6
  *
7
7
  * Pure: no imports and no services, so it also runs offline under Lune (scripts/test-analytics.luau). The JSON
@@ -215,11 +215,11 @@ local function parseSettings(raw, decode)
215
215
  errors = errors,
216
216
  }
217
217
  end
218
- --* Where identity rows go on a Basin game: the fleet API (`TypeTorchFleet` = { url, token }) at /v1/identity.
218
+ --* Where identity rows go on a Basin game: the fleet API (the settings' `fleet` = { url, token }) at /v1/identity.
219
219
  --[[
220
220
  *
221
221
  * The identity target for these settings: duckdb none (identities ride in the batch body); basin the `identity` URL,
222
- * else the fleet API's /v1/identity from the `TypeTorchFleet` value (a table or JSON `{ url, token }`), else none.
222
+ * else the fleet API's /v1/identity from the settings' `fleet` value (a table or JSON `{ url, token }`), else none.
223
223
 
224
224
  ]]
225
225
  local function identityTarget(settings, fleetRaw, decode)
@@ -0,0 +1,26 @@
1
+ import type { Trove } from "@rbxts/trove";
2
+ /**
3
+ * `TypeTorch.runDetached(fn)` (server only, kernel 0.3.8): runs `fn` on a thread the KERNEL owns, so a deploy's swap
4
+ * (this generation's stop, and its hard stop, which kills the generation's own threads mid-call) never cuts it off.
5
+ * Made for library calls that must not stop halfway: a ProfileStore / ProfileService load, save or release cut off
6
+ * by a hard stop jams that player's profile on this server (docs: Player data, "Why the jobs").
7
+ *
8
+ * TypeTorch.runDetached(() => store.StartSessionAsync(key)).then((profile) => ...);
9
+ *
10
+ * - The Promise settles on this generation's thread while it runs. A result that arrives after this generation
11
+ * stopped is dropped: a job whose result must survive a swap writes it into `persist` itself.
12
+ * - A job keeps this generation's closures (and everything they reference) alive until it ends: keep jobs short.
13
+ * - Errors reject the Promise and go to the kernel's log with the generation's name; they never count toward the
14
+ * build's health window. At most 256 jobs run at once per server (then it throws); one past 60 s is logged.
15
+ * - Older kernels: throws "needs kernel 0.3.8; use the DataHost job queue (Player data guide)". Clients: throws.
16
+ */
17
+ /** The first kernel with `api:runDetached`. */
18
+ export declare const DETACHED_KERNEL = "0.3.8";
19
+ /** Called by bindTypeTorch. */
20
+ export declare function bindDetached(realm: "server" | "client", kernel: unknown, trove: Trove, kernelVersion: string): void;
21
+ /** Called by unbindTypeTorch. */
22
+ export declare function unbindDetached(): void;
23
+ /** `runDetached` reaches a kernel thread (a server on kernel 0.3.8+). */
24
+ export declare function detachedSupported(): boolean;
25
+ /** `TypeTorch.runDetached`. */
26
+ export declare function runDetached<T>(fn: () => T): Promise<T>;