@typetorch/framework 0.5.0 → 0.5.3
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.
- package/LICENSE +22 -22
- package/README.md +4 -3
- package/out/analytics/SCHEMA.md +3 -2
- package/out/analytics/experiments.d.ts +7 -1
- package/out/analytics/experiments.luau +43 -1
- package/out/analytics/schema.d.ts +7 -0
- package/out/analytics/server.d.ts +1 -0
- package/out/analytics/server.luau +27 -10
- package/out/analytics/settings.d.ts +2 -0
- package/out/analytics/settings.luau +37 -0
- package/out/assets/manifest.d.ts +29 -0
- package/out/assets/manifest.luau +92 -0
- package/out/assets/sync.luau +33 -8
- package/out/devtools/assets-ui.luau +14 -1
- package/out/devtools/client.d.ts +0 -5
- package/out/devtools/client.luau +82 -5
- package/out/devtools/health.d.ts +1 -1
- package/out/devtools/health.luau +1 -1
- package/out/kernel.d.ts +3 -0
- package/out/runtime/safe-clean.d.ts +11 -0
- package/out/runtime/safe-clean.luau +57 -0
- package/out/runtime/start.luau +26 -12
- package/out/settings.luau +2 -1
- package/out/version.d.ts +1 -1
- package/out/version.luau +1 -1
- package/package.json +53 -53
package/LICENSE
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 TypeTorch contributors
|
|
4
|
-
Copyright (c) 2021 Flamework (src/reflection: the Reflect and Modding runtime, adapted from @flamework/core)
|
|
5
|
-
|
|
6
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
-
in the Software without restriction, including without limitation the rights
|
|
9
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
-
furnished to do so, subject to the following conditions:
|
|
12
|
-
|
|
13
|
-
The above copyright notice and this permission notice shall be included in all
|
|
14
|
-
copies or substantial portions of the Software.
|
|
15
|
-
|
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TypeTorch contributors
|
|
4
|
+
Copyright (c) 2021 Flamework (src/reflection: the Reflect and Modding runtime, adapted from @flamework/core)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -427,7 +427,7 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
|
|
|
427
427
|
row carries a UserId.
|
|
428
428
|
- **Identities** (option `identity`, default on): once the pid is known, one row `{ pid, uid, t }` (the UserId, nothing
|
|
429
429
|
else) per session goes to the dev's own server only, never into the events: duckdb games in the batch body, Basin
|
|
430
|
-
games to the
|
|
430
|
+
games to the backend (the record's `backend` url + `/v1/identity`, or the `identity` / `identityToken` settings). The
|
|
431
431
|
analytics server keeps pid -> UserId in a deletable table (support lookups, Right to Erasure).
|
|
432
432
|
- **Player ids:** DataStore `TypeTorchAnalytics`, key `p/<UserId>` -> `{ pid, first, last }`: one read per join, a
|
|
433
433
|
write on the first join and at leave. Deleting the key (Right to Erasure) leaves that player's rows anonymous.
|
|
@@ -465,8 +465,9 @@ count affected players by pid, never by name) and counts its own DataStore and H
|
|
|
465
465
|
menu **Server > Budget**: bars per Roblox limit for the player count, who spends it, the DataStore budget left, memory).
|
|
466
466
|
|
|
467
467
|
**Settings** (server only, never sent to clients): the signed settings record's `analytics` field (kernel 0.3.8,
|
|
468
|
-
plans/20),
|
|
469
|
-
|
|
468
|
+
plans/20), derived from `backend` by `typetorch backend setup` (`writeBackendSettings()` in `@typetorch/backend` runs it, and
|
|
469
|
+
`bun run local` too). Every new copy applies live within seconds (the CLI pings servers). A Basin sink has no CLI writer
|
|
470
|
+
since CLI 0.9: `backend setup` replaces it. Before kernel 0.3.8
|
|
470
471
|
there are no settings unless the game passes `new AnalyticsEngine({ settings })`:
|
|
471
472
|
|
|
472
473
|
```json
|
package/out/analytics/SCHEMA.md
CHANGED
|
@@ -65,6 +65,7 @@ First-ever-session detail, one row per packed chunk.
|
|
|
65
65
|
| tech | `load` (src client) | `secs`: from the client starting to the engine starting (first client generation only) |
|
|
66
66
|
| tech | `error` | Server or client (src). `msg` (first 300 chars, player names replaced by `<player>`), `n` (times in the window). At most 10 per tech tick (server), 5 per 30 s and 100 per session (client) |
|
|
67
67
|
| tech | `start` | First generation of a server that runs the engine: `gen`, `kernel`, `load?` |
|
|
68
|
+
| tech | `assets` | Each generation whose artifact has hot assets (or an unusable manifest), at its start: `gen`, `boot` (the server's first generation), `manifest` (`ok`, `invalid`, `unread`), `place?` (`same`: the manifest names this place version; `vouched`: the place's backup vouches for it; `differs`: keys without a matching baked copy load from their uploads; `unknown`), `mpv?` (the manifest's place version), `pv?` (game.PlaceVersion), `keys`, `kept`, `baked`, `loaded`, `failed`, `loading` (keys per AssetSync source), `missing` (keys with nothing live), `late` (loaded after the start-up wait), `timedOut`, `ms?`, `first?` (the first failure or manifest error) |
|
|
68
69
|
| tech | `swap` / `rollback` | A generation started by a hot swap (`rollback` for rollback reasons), 5 s after it started: `reason`, `from?` (previous artifact id), `gen`, `load?`, `stop?`, `swap?` (seconds), `players` |
|
|
69
70
|
| tech | `swap_out` | Just before a generation stops: `reason`, `next?` (next artifact id), `players` |
|
|
70
71
|
| zone | `enter` | `zone`, `from` (previous zone or `""`) |
|
|
@@ -142,7 +143,7 @@ repo's `bun run local` does it). Every new signed copy applies at once, so dials
|
|
|
142
143
|
"flushSeconds": 15,
|
|
143
144
|
"recordShare": 1,
|
|
144
145
|
"techEvery": 60,
|
|
145
|
-
"experiments": { "onboarding": { "active": true, "weights": [1, 1], "variant": "short" } }
|
|
146
|
+
"experiments": { "onboarding": { "active": true, "weights": [1, 1], "variant": "short", "players": { "123456": "long" } } }
|
|
146
147
|
}
|
|
147
148
|
```
|
|
148
149
|
|
|
@@ -155,7 +156,7 @@ repo's `bun run local` does it). Every new signed copy applies at once, so dials
|
|
|
155
156
|
| `flushSeconds` | 15 | 5..300 |
|
|
156
157
|
| `recordShare` | 1 | 0..1, share of new players recorded (stable per pid) |
|
|
157
158
|
| `techEvery` | 60 | 15..3600 seconds between tech samples |
|
|
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) |
|
|
159
|
+
| `experiments` | none | Per experiment: `active: false` (everyone gets the first variant, not stamped), `weights` (per variant, in the game's order), `variant` (force one), `players` (`{ [UserId or pid]: variant }`, at most 200: forces one player's variant over everything else, stamped `forced`; each client's hello gets only its own entry). The CLI writes them: `typetorch experiment` |
|
|
159
160
|
| `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
161
|
| `identityToken` | the settings' `fleet` token | basin: the token for `identity`. Never logged |
|
|
161
162
|
|
|
@@ -19,10 +19,16 @@ export interface Assignment {
|
|
|
19
19
|
/**
|
|
20
20
|
* The variant of experiment `name` for player `pid`. The first variant is the control: everyone gets it while the
|
|
21
21
|
* experiment is switched off (`active: false`). `weights` split players unevenly (`[9, 1]`: 10% get the second);
|
|
22
|
-
* `variant` forces one for everyone
|
|
22
|
+
* `variant` forces one for everyone; `player` (one player's entry of `players`, see `playerOverride`) forces one for
|
|
23
|
+
* this player and wins over the rest. The point in [0, 1) is unitHash(`<name>:<pid>`), so changing weights moves as few
|
|
23
24
|
* players as possible.
|
|
24
25
|
*/
|
|
25
26
|
export declare function assignVariant(pid: string, name: string, variants: string[], override?: ExperimentOverride): Assignment;
|
|
27
|
+
/**
|
|
28
|
+
* One player's view of an override: `players` resolved to `player` (by UserId, else pid) and dropped, so a client's
|
|
29
|
+
* hello never carries other players' ids.
|
|
30
|
+
*/
|
|
31
|
+
export declare function playerOverride(override: ExperimentOverride | undefined, userId: number | undefined, pid: string): ExperimentOverride | undefined;
|
|
26
32
|
/** Whether player `pid` is in the recorded share (0..1) of new players. Stable per player. */
|
|
27
33
|
export declare function inShare(pid: string, share: number): boolean;
|
|
28
34
|
/** A valid experiment name: 1-64 chars, letters, digits, `_`, `-`, `.`, `/`. */
|
|
@@ -44,7 +44,8 @@ end
|
|
|
44
44
|
*
|
|
45
45
|
* The variant of experiment `name` for player `pid`. The first variant is the control: everyone gets it while the
|
|
46
46
|
* experiment is switched off (`active: false`). `weights` split players unevenly (`[9, 1]`: 10% get the second);
|
|
47
|
-
* `variant` forces one for everyone
|
|
47
|
+
* `variant` forces one for everyone; `player` (one player's entry of `players`, see `playerOverride`) forces one for
|
|
48
|
+
* this player and wins over the rest. The point in [0, 1) is unitHash(`<name>:<pid>`), so changing weights moves as few
|
|
48
49
|
* players as possible.
|
|
49
50
|
|
|
50
51
|
]]
|
|
@@ -61,6 +62,18 @@ local function assignVariant(pid, name, variants, override)
|
|
|
61
62
|
forced = false,
|
|
62
63
|
}
|
|
63
64
|
end
|
|
65
|
+
local _mine = override
|
|
66
|
+
if _mine ~= nil then
|
|
67
|
+
_mine = _mine.player
|
|
68
|
+
end
|
|
69
|
+
local mine = _mine
|
|
70
|
+
if type(mine) == "string" and table.find(variants, mine) ~= nil then
|
|
71
|
+
return {
|
|
72
|
+
variant = mine,
|
|
73
|
+
active = true,
|
|
74
|
+
forced = true,
|
|
75
|
+
}
|
|
76
|
+
end
|
|
64
77
|
local _result = override
|
|
65
78
|
if _result ~= nil then
|
|
66
79
|
_result = _result.active
|
|
@@ -129,6 +142,34 @@ local function assignVariant(pid, name, variants, override)
|
|
|
129
142
|
forced = false,
|
|
130
143
|
}
|
|
131
144
|
end
|
|
145
|
+
--[[
|
|
146
|
+
*
|
|
147
|
+
* One player's view of an override: `players` resolved to `player` (by UserId, else pid) and dropped, so a client's
|
|
148
|
+
* hello never carries other players' ids.
|
|
149
|
+
|
|
150
|
+
]]
|
|
151
|
+
local function playerOverride(override, userId, pid)
|
|
152
|
+
if override == nil then
|
|
153
|
+
return nil
|
|
154
|
+
end
|
|
155
|
+
local result = {
|
|
156
|
+
active = override.active,
|
|
157
|
+
weights = override.weights,
|
|
158
|
+
variant = override.variant,
|
|
159
|
+
}
|
|
160
|
+
local players = override.players
|
|
161
|
+
if players ~= nil then
|
|
162
|
+
local _condition = (if userId ~= nil then players[tostring(userId)] else nil)
|
|
163
|
+
if _condition == nil then
|
|
164
|
+
_condition = players[pid]
|
|
165
|
+
end
|
|
166
|
+
local mine = _condition
|
|
167
|
+
if mine ~= nil then
|
|
168
|
+
result.player = mine
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
return result
|
|
172
|
+
end
|
|
132
173
|
--* Whether player `pid` is in the recorded share (0..1) of new players. Stable per player.
|
|
133
174
|
local function inShare(pid, share)
|
|
134
175
|
if share >= 1 then
|
|
@@ -182,6 +223,7 @@ return {
|
|
|
182
223
|
hash32 = hash32,
|
|
183
224
|
unitHash = unitHash,
|
|
184
225
|
assignVariant = assignVariant,
|
|
226
|
+
playerOverride = playerOverride,
|
|
185
227
|
inShare = inShare,
|
|
186
228
|
isExperimentName = isExperimentName,
|
|
187
229
|
isVariantList = isVariantList,
|
|
@@ -86,6 +86,13 @@ export interface ExperimentOverride {
|
|
|
86
86
|
weights?: number[];
|
|
87
87
|
/** Everyone gets this variant (must be one of the game's variants). */
|
|
88
88
|
variant?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Per-player forced variants, keyed by UserId (`"123456"`) or pid (32 hex): wins over `active`, `variant` and
|
|
91
|
+
* `weights` for that player (QA, support). Server-only: a client gets only its own entry, as `player`.
|
|
92
|
+
*/
|
|
93
|
+
players?: Record<string, string>;
|
|
94
|
+
/** This player's forced variant (resolved from `players` by `playerOverride`; what a client's hello carries). */
|
|
95
|
+
player?: string;
|
|
89
96
|
}
|
|
90
97
|
/** The sink settings: the signed settings' `analytics` (kernel 0.3.8, server-only), or `new AnalyticsEngine({ settings })`. */
|
|
91
98
|
export interface AnalyticsSettings {
|
|
@@ -101,6 +101,7 @@ export declare class ServerAnalytics {
|
|
|
101
101
|
/** Activity: per player, or server-wide (every player without their own) when `player` is undefined. */
|
|
102
102
|
setActivity(player: Player | undefined, activity: string | undefined): void;
|
|
103
103
|
setScreen(session: PlayerSession, screen: string, src: "server" | "client", t?: number, props?: string): void;
|
|
104
|
+
/** The overrides as this player sees them (their own `players` entry only; hello). */
|
|
104
105
|
private overrides;
|
|
105
106
|
/** The player's variant (stamped on their later events while active). May yield until the player's pid loads. */
|
|
106
107
|
experiment(player: Player, name: string, variants: string[]): string;
|
|
@@ -19,6 +19,8 @@ local _typetorch = TS.import(script, script.Parent.Parent, "typetorch")
|
|
|
19
19
|
local closeHooksSupported = _typetorch.closeHooksSupported
|
|
20
20
|
local onGenerationClose = _typetorch.onGenerationClose
|
|
21
21
|
local TypeTorch = _typetorch.TypeTorch
|
|
22
|
+
local assetCounts = TS.import(script, script.Parent.Parent, "assets", "manifest").assetCounts
|
|
23
|
+
local assetReport = TS.import(script, script.Parent.Parent, "assets", "sync").assetReport
|
|
22
24
|
local toBase64 = TS.import(script, script.Parent, "codec").toBase64
|
|
23
25
|
local _props = TS.import(script, script.Parent, "props")
|
|
24
26
|
local encodeProps = _props.encodeProps
|
|
@@ -28,6 +30,7 @@ local assignVariant = _experiments.assignVariant
|
|
|
28
30
|
local inShare = _experiments.inShare
|
|
29
31
|
local isExperimentName = _experiments.isExperimentName
|
|
30
32
|
local isVariantList = _experiments.isVariantList
|
|
33
|
+
local playerOverride = _experiments.playerOverride
|
|
31
34
|
local _protocol = TS.import(script, script.Parent, "protocol")
|
|
32
35
|
local CHANNEL = _protocol.CHANNEL
|
|
33
36
|
local CHUNK_MAX_BYTES = _protocol.CHUNK_MAX_BYTES
|
|
@@ -494,7 +497,7 @@ do
|
|
|
494
497
|
end
|
|
495
498
|
function ServerAnalytics:track(player, kind, name, props)
|
|
496
499
|
if not isEventName(name) then
|
|
497
|
-
warn("[src/analytics/server.ts:
|
|
500
|
+
warn("[src/analytics/server.ts:473]", `[analytics] {kind} event name must be 1-64 characters: {string.sub(tostring(name), 1, 80)}`)
|
|
498
501
|
return nil
|
|
499
502
|
end
|
|
500
503
|
local session = if player then self:sessionOf(player) else nil
|
|
@@ -586,11 +589,11 @@ do
|
|
|
586
589
|
end
|
|
587
590
|
_self:emit(_exp, "state", "screen", _condition, src, t)
|
|
588
591
|
end
|
|
589
|
-
function ServerAnalytics:overrides()
|
|
592
|
+
function ServerAnalytics:overrides(player, session)
|
|
590
593
|
local result = {}
|
|
591
594
|
if self.settings then
|
|
592
595
|
for name, override in self.settings.experiments do
|
|
593
|
-
result[name] = override
|
|
596
|
+
result[name] = playerOverride(override, player.UserId, session.pid)
|
|
594
597
|
end
|
|
595
598
|
end
|
|
596
599
|
return result
|
|
@@ -602,7 +605,7 @@ do
|
|
|
602
605
|
end
|
|
603
606
|
local control = _condition
|
|
604
607
|
if not isExperimentName(name) or not isVariantList(variants) then
|
|
605
|
-
warn("[src/analytics/server.ts:
|
|
608
|
+
warn("[src/analytics/server.ts:525]", `[analytics] experiment("{string.sub(tostring(name), 1, 64)}"): bad name or variants (1-16 distinct names)`)
|
|
606
609
|
return control
|
|
607
610
|
end
|
|
608
611
|
local session = self:sessionOf(player)
|
|
@@ -641,7 +644,7 @@ do
|
|
|
641
644
|
local _name_1 = name
|
|
642
645
|
_result = _experiments_1[_name_1]
|
|
643
646
|
end
|
|
644
|
-
local assignment = assignVariant(_exp_1, _exp_2, _exp_3, _result)
|
|
647
|
+
local assignment = assignVariant(_exp_1, _exp_2, _exp_3, playerOverride(_result, player.UserId, session.pid))
|
|
645
648
|
local _exp_4 = session.exp
|
|
646
649
|
local _name_1 = name
|
|
647
650
|
local current = _exp_4[_name_1]
|
|
@@ -667,7 +670,7 @@ do
|
|
|
667
670
|
function ServerAnalytics:applySettings(parsed)
|
|
668
671
|
local errorsText = table.concat(parsed.errors, "; ")
|
|
669
672
|
if errorsText ~= table.concat(self.settingsErrors, "; ") and errorsText ~= "" then
|
|
670
|
-
warn("[src/analytics/server.ts:
|
|
673
|
+
warn("[src/analytics/server.ts:560]", `[analytics] the sink settings (settings.backend or settings.analytics): {errorsText}`)
|
|
671
674
|
end
|
|
672
675
|
self.settingsErrors = parsed.errors
|
|
673
676
|
local hadSettings = self.settings ~= nil
|
|
@@ -697,7 +700,7 @@ do
|
|
|
697
700
|
-- `new AnalyticsEngine({ settings })`.
|
|
698
701
|
if not settingsSupported() then
|
|
699
702
|
if self.kernel ~= nil and self.kernel.test ~= true then
|
|
700
|
-
warn("[src/analytics/server.ts:
|
|
703
|
+
warn("[src/analytics/server.ts:587]", `[analytics] no sink settings: they come from the signed settings record (kernel 0.3.8+, "typetorch backend setup"); this server runs kernel {self.kernel.kernelVersion}. Or pass new AnalyticsEngine(\{ settings \})`)
|
|
701
704
|
end
|
|
702
705
|
self.settingsTried = true
|
|
703
706
|
return nil
|
|
@@ -1226,6 +1229,20 @@ do
|
|
|
1226
1229
|
load = start.loadSeconds,
|
|
1227
1230
|
}))
|
|
1228
1231
|
end
|
|
1232
|
+
-- Hot assets: how this generation's AssetSync went (it ran before the modules loaded), so "every new server
|
|
1233
|
+
-- loads its hot assets from uploads" shows in the events table (2026-10-13 incident).
|
|
1234
|
+
local assets = assetReport()
|
|
1235
|
+
if assets.manifest ~= "none" or #assets.errors > 0 then
|
|
1236
|
+
local _self = self
|
|
1237
|
+
local _object = {
|
|
1238
|
+
gen = generation,
|
|
1239
|
+
boot = start.kind ~= "swap",
|
|
1240
|
+
}
|
|
1241
|
+
for _k, _v in assetCounts(assets) do
|
|
1242
|
+
_object[_k] = _v
|
|
1243
|
+
end
|
|
1244
|
+
_self:emit(nil, "tech", "assets", encodeProps(_object))
|
|
1245
|
+
end
|
|
1229
1246
|
end
|
|
1230
1247
|
self.trove:add(TypeTorch:onSwapOut(function(info)
|
|
1231
1248
|
local _self = self
|
|
@@ -1273,7 +1290,7 @@ do
|
|
|
1273
1290
|
_condition = DEFAULT_TECH_EVERY
|
|
1274
1291
|
end
|
|
1275
1292
|
_object[_left] = _condition
|
|
1276
|
-
_object.exps = self:overrides()
|
|
1293
|
+
_object.exps = self:overrides(player, session)
|
|
1277
1294
|
local hello = _object
|
|
1278
1295
|
dispatcher.kernel:send(player, CHANNEL, "hi", hello)
|
|
1279
1296
|
end
|
|
@@ -1499,7 +1516,7 @@ do
|
|
|
1499
1516
|
end
|
|
1500
1517
|
end)
|
|
1501
1518
|
if not ok then
|
|
1502
|
-
warn("[src/analytics/server.ts:
|
|
1519
|
+
warn("[src/analytics/server.ts:1086]", `[analytics] client message failed: {string.sub(tostring(err), 1, 200)}`)
|
|
1503
1520
|
end
|
|
1504
1521
|
end)
|
|
1505
1522
|
self.trove:add(function()
|
|
@@ -1782,7 +1799,7 @@ do
|
|
|
1782
1799
|
self.lastWarnAt = now
|
|
1783
1800
|
local more = if self.suppressed > 0 then ` (+{self.suppressed} more in the last minute)` else ""
|
|
1784
1801
|
self.suppressed = 0
|
|
1785
|
-
warn("[src/analytics/server.ts:
|
|
1802
|
+
warn("[src/analytics/server.ts:1281]", `[analytics] {message}{more}`)
|
|
1786
1803
|
return true
|
|
1787
1804
|
end
|
|
1788
1805
|
function ServerAnalytics:noteSuccess()
|
|
@@ -13,6 +13,8 @@ export interface ParsedSettings {
|
|
|
13
13
|
settings?: ResolvedSettings;
|
|
14
14
|
errors: string[];
|
|
15
15
|
}
|
|
16
|
+
/** Per-player overrides one experiment may hold (`players`). */
|
|
17
|
+
export declare const EXPERIMENT_PLAYERS_MAX = 200;
|
|
16
18
|
/**
|
|
17
19
|
* Reads the settings value: a table (the configs API stores JSON objects) or a JSON string. `undefined` (no key) gives
|
|
18
20
|
* no settings and no errors. Unknown fields are ignored.
|
|
@@ -35,6 +35,19 @@ local function clampNumber(value, fallback, low, high, label, errors)
|
|
|
35
35
|
end
|
|
36
36
|
return math.clamp(value, low, high)
|
|
37
37
|
end
|
|
38
|
+
--* Per-player overrides one experiment may hold (`players`).
|
|
39
|
+
local EXPERIMENT_PLAYERS_MAX = 200
|
|
40
|
+
--* A `players` key: a UserId (digits) or a pid (32 lowercase hex).
|
|
41
|
+
local function isPlayerKey(key)
|
|
42
|
+
local _key = key
|
|
43
|
+
if not (type(_key) == "string") then
|
|
44
|
+
return false
|
|
45
|
+
end
|
|
46
|
+
if #key >= 1 and #key <= 19 and (string.match(key, "^%d+$")) ~= nil then
|
|
47
|
+
return true
|
|
48
|
+
end
|
|
49
|
+
return #key == 32 and (string.match(key, "^[0-9a-f]+$")) ~= nil
|
|
50
|
+
end
|
|
38
51
|
local function parseExperiments(value, errors)
|
|
39
52
|
local result = {}
|
|
40
53
|
if value == nil then
|
|
@@ -103,6 +116,29 @@ local function parseExperiments(value, errors)
|
|
|
103
116
|
table.insert(_errors, _arg0)
|
|
104
117
|
end
|
|
105
118
|
end
|
|
119
|
+
if entry.players ~= nil then
|
|
120
|
+
local players = {}
|
|
121
|
+
local _players = entry.players
|
|
122
|
+
local ok = type(_players) == "table"
|
|
123
|
+
local count = 0
|
|
124
|
+
if ok then
|
|
125
|
+
for key, variant in pairs(entry.players) do
|
|
126
|
+
count += 1
|
|
127
|
+
if not isPlayerKey(key) or not (type(variant) == "string") or #variant < 1 or #variant > 32 then
|
|
128
|
+
ok = false
|
|
129
|
+
else
|
|
130
|
+
players[key] = variant
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
if ok and count <= EXPERIMENT_PLAYERS_MAX then
|
|
135
|
+
override.players = players
|
|
136
|
+
else
|
|
137
|
+
local _errors = errors
|
|
138
|
+
local _arg0 = `experiments.{name}.players must map UserIds or pids to variant names (at most {EXPERIMENT_PLAYERS_MAX})`
|
|
139
|
+
table.insert(_errors, _arg0)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
106
142
|
result[name] = override
|
|
107
143
|
end
|
|
108
144
|
return result
|
|
@@ -326,4 +362,5 @@ return {
|
|
|
326
362
|
recordsSupported = recordsSupported,
|
|
327
363
|
DEFAULT_FLUSH_SECONDS = DEFAULT_FLUSH_SECONDS,
|
|
328
364
|
DEFAULT_TECH_EVERY = DEFAULT_TECH_EVERY,
|
|
365
|
+
EXPERIMENT_PLAYERS_MAX = EXPERIMENT_PLAYERS_MAX,
|
|
329
366
|
}
|
package/out/assets/manifest.d.ts
CHANGED
|
@@ -23,6 +23,14 @@ export declare const ATTR_VERSION = "TypeTorchAssetVersion";
|
|
|
23
23
|
* a template are tagged too: only an instance whose Parent is still this path counts as the live one.
|
|
24
24
|
*/
|
|
25
25
|
export declare const ATTR_PATH = "TypeTorchAssetPath";
|
|
26
|
+
/**
|
|
27
|
+
* The place's backup build (ServerStorage.TypeTorchBackup) vouches for the place's hot-asset copies after the CLI's
|
|
28
|
+
* backup refresh published the place (cli commands/backup.ts, kernelpatch-task step 4b): `HotAssetsPlaceVersion` is the
|
|
29
|
+
* lockfile placeVersion the copies match, `HotAssetsSavedAs` the place version that save became.
|
|
30
|
+
*/
|
|
31
|
+
export declare const BACKUP_NAME = "TypeTorchBackup";
|
|
32
|
+
export declare const ATTR_BACKUP_PLACE_VERSION = "HotAssetsPlaceVersion";
|
|
33
|
+
export declare const ATTR_BACKUP_SAVED_AS = "HotAssetsSavedAs";
|
|
26
34
|
export declare const MAX_KEY_LENGTH = 64;
|
|
27
35
|
export type AssetRealm = "replicated" | "server";
|
|
28
36
|
/** One manifest entry (one key), validated. */
|
|
@@ -95,6 +103,20 @@ export interface AssetDecision {
|
|
|
95
103
|
*/
|
|
96
104
|
fallbackBaked?: number;
|
|
97
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Whether this server's place holds exactly the copies the manifest was exported from:
|
|
108
|
+
* - same: the manifest names this place version;
|
|
109
|
+
* - vouched: it names another one, but the place's backup says this version is a save of that one that changed nothing
|
|
110
|
+
* outside the kernel slots (`backup` = its HotAssetsPlaceVersion / HotAssetsSavedAs);
|
|
111
|
+
* - differs: neither (new servers load every key whose baked copy has no matching hash);
|
|
112
|
+
* - unknown: the manifest names no place version.
|
|
113
|
+
*/
|
|
114
|
+
export type PlaceMatch = "same" | "vouched" | "differs" | "unknown";
|
|
115
|
+
export interface BackupVouch {
|
|
116
|
+
placeVersion?: number;
|
|
117
|
+
savedAs?: number;
|
|
118
|
+
}
|
|
119
|
+
export declare function placeMatchesManifest(manifestVersion: number | undefined, placeVersion: number, backup?: BackupVouch): PlaceMatch;
|
|
98
120
|
/**
|
|
99
121
|
* What AssetSync does for one key:
|
|
100
122
|
* 1. keep the tagged copy whose TypeTorchAssetVersion is the manifest's `ver` (the newest one if several);
|
|
@@ -128,6 +150,11 @@ export interface AssetSyncReport {
|
|
|
128
150
|
manifest: "none" | "ok" | "invalid" | "unread";
|
|
129
151
|
/** Where the manifest was found: "mount", "Server" (the payload's Server folder) or "payload" (re-read). */
|
|
130
152
|
from?: string;
|
|
153
|
+
/** placeMatchesManifest: whether the place's copies are the manifest's ("differs": the upload path). */
|
|
154
|
+
place?: PlaceMatch;
|
|
155
|
+
/** The manifest's placeVersion and this server's game.PlaceVersion. */
|
|
156
|
+
manifestPlaceVersion?: number;
|
|
157
|
+
placeVersion?: number;
|
|
131
158
|
errors: string[];
|
|
132
159
|
/** AssetSync is still holding the generation's start. */
|
|
133
160
|
running: boolean;
|
|
@@ -138,6 +165,8 @@ export interface AssetSyncReport {
|
|
|
138
165
|
/** Tagged keys this manifest doesn't name: left as they are. */
|
|
139
166
|
unmanaged: string[];
|
|
140
167
|
}
|
|
168
|
+
/** Keys per source, and the rest of what the `tech` / `assets` analytics row carries (SCHEMA.md). */
|
|
169
|
+
export declare function assetCounts(report: AssetSyncReport): Record<string, unknown>;
|
|
141
170
|
/** What Server > Status needs (devtools ServerFacts.assets). */
|
|
142
171
|
export interface AssetFacts {
|
|
143
172
|
failed: number;
|
package/out/assets/manifest.luau
CHANGED
|
@@ -28,6 +28,16 @@ local ATTR_VERSION = "TypeTorchAssetVersion"
|
|
|
28
28
|
|
|
29
29
|
]]
|
|
30
30
|
local ATTR_PATH = "TypeTorchAssetPath"
|
|
31
|
+
--[[
|
|
32
|
+
*
|
|
33
|
+
* The place's backup build (ServerStorage.TypeTorchBackup) vouches for the place's hot-asset copies after the CLI's
|
|
34
|
+
* backup refresh published the place (cli commands/backup.ts, kernelpatch-task step 4b): `HotAssetsPlaceVersion` is the
|
|
35
|
+
* lockfile placeVersion the copies match, `HotAssetsSavedAs` the place version that save became.
|
|
36
|
+
|
|
37
|
+
]]
|
|
38
|
+
local BACKUP_NAME = "TypeTorchBackup"
|
|
39
|
+
local ATTR_BACKUP_PLACE_VERSION = "HotAssetsPlaceVersion"
|
|
40
|
+
local ATTR_BACKUP_SAVED_AS = "HotAssetsSavedAs"
|
|
31
41
|
local MAX_KEY_LENGTH = 64
|
|
32
42
|
local MAX_ASSETS = 1000
|
|
33
43
|
local MAX_ERRORS = 20
|
|
@@ -255,6 +265,28 @@ end
|
|
|
255
265
|
-- The decision --------------------------------------------------------------------------------------------------------
|
|
256
266
|
--* A live (tagged) copy of the key: its TypeTorchAssetVersion. Only copies whose Parent is their own path count.
|
|
257
267
|
--* An untagged copy of the key under the entry's path (the place's baked copy): its TypeTorchAssetHash.
|
|
268
|
+
--[[
|
|
269
|
+
*
|
|
270
|
+
* Whether this server's place holds exactly the copies the manifest was exported from:
|
|
271
|
+
* - same: the manifest names this place version;
|
|
272
|
+
* - vouched: it names another one, but the place's backup says this version is a save of that one that changed nothing
|
|
273
|
+
* outside the kernel slots (`backup` = its HotAssetsPlaceVersion / HotAssetsSavedAs);
|
|
274
|
+
* - differs: neither (new servers load every key whose baked copy has no matching hash);
|
|
275
|
+
* - unknown: the manifest names no place version.
|
|
276
|
+
|
|
277
|
+
]]
|
|
278
|
+
local function placeMatchesManifest(manifestVersion, placeVersion, backup)
|
|
279
|
+
if manifestVersion == nil then
|
|
280
|
+
return "unknown"
|
|
281
|
+
end
|
|
282
|
+
if manifestVersion == placeVersion then
|
|
283
|
+
return "same"
|
|
284
|
+
end
|
|
285
|
+
if backup ~= nil and backup.placeVersion == manifestVersion and backup.savedAs == placeVersion then
|
|
286
|
+
return "vouched"
|
|
287
|
+
end
|
|
288
|
+
return "differs"
|
|
289
|
+
end
|
|
258
290
|
local function indexes(count, except)
|
|
259
291
|
local list = {}
|
|
260
292
|
do
|
|
@@ -328,6 +360,61 @@ local function decideAsset(entry, tagged, baked, placeMatches)
|
|
|
328
360
|
end
|
|
329
361
|
-- Status (devtools op "assets", Server > Status) ------------------------------------------------------------------------
|
|
330
362
|
--* kept: already live with the manifest version; baked: the place's copy adopted; loaded; failed (see `error`).
|
|
363
|
+
--* Keys per source, and the rest of what the `tech` / `assets` analytics row carries (SCHEMA.md).
|
|
364
|
+
local function assetCounts(report)
|
|
365
|
+
local counts = {
|
|
366
|
+
kept = 0,
|
|
367
|
+
baked = 0,
|
|
368
|
+
loaded = 0,
|
|
369
|
+
failed = 0,
|
|
370
|
+
loading = 0,
|
|
371
|
+
}
|
|
372
|
+
local missing = 0
|
|
373
|
+
local late = 0
|
|
374
|
+
local first
|
|
375
|
+
for _, status in report.entries do
|
|
376
|
+
local _condition = counts[status.source]
|
|
377
|
+
if _condition == nil then
|
|
378
|
+
_condition = 0
|
|
379
|
+
end
|
|
380
|
+
counts[status.source] = _condition + 1
|
|
381
|
+
if not status.live then
|
|
382
|
+
missing += 1
|
|
383
|
+
end
|
|
384
|
+
if status.late == true then
|
|
385
|
+
late += 1
|
|
386
|
+
end
|
|
387
|
+
if status.source == "failed" and first == nil then
|
|
388
|
+
local _exp = status.key
|
|
389
|
+
local _condition_1 = status.error
|
|
390
|
+
if _condition_1 == nil then
|
|
391
|
+
_condition_1 = "failed"
|
|
392
|
+
end
|
|
393
|
+
first = `{_exp}: {_condition_1}`
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
local _object = {
|
|
397
|
+
manifest = report.manifest,
|
|
398
|
+
place = report.place,
|
|
399
|
+
mpv = report.manifestPlaceVersion,
|
|
400
|
+
pv = report.placeVersion,
|
|
401
|
+
keys = #report.entries,
|
|
402
|
+
}
|
|
403
|
+
for _k, _v in counts do
|
|
404
|
+
_object[_k] = _v
|
|
405
|
+
end
|
|
406
|
+
_object.missing = missing
|
|
407
|
+
_object.late = late
|
|
408
|
+
_object.timedOut = report.timedOut == true
|
|
409
|
+
_object.ms = report.ms
|
|
410
|
+
local _left = "first"
|
|
411
|
+
local _condition = first
|
|
412
|
+
if _condition == nil then
|
|
413
|
+
_condition = report.errors[1]
|
|
414
|
+
end
|
|
415
|
+
_object[_left] = _condition
|
|
416
|
+
return _object
|
|
417
|
+
end
|
|
331
418
|
--* What Server > Status needs (devtools ServerFacts.assets).
|
|
332
419
|
local function summarizeAssets(report)
|
|
333
420
|
if report.manifest == "none" and #report.errors == 0 then
|
|
@@ -368,7 +455,9 @@ return {
|
|
|
368
455
|
pathSegments = pathSegments,
|
|
369
456
|
realmOfPath = realmOfPath,
|
|
370
457
|
parseManifest = parseManifest,
|
|
458
|
+
placeMatchesManifest = placeMatchesManifest,
|
|
371
459
|
decideAsset = decideAsset,
|
|
460
|
+
assetCounts = assetCounts,
|
|
372
461
|
summarizeAssets = summarizeAssets,
|
|
373
462
|
ASSET_TAG_PREFIX = ASSET_TAG_PREFIX,
|
|
374
463
|
ATTR_KEY = ATTR_KEY,
|
|
@@ -376,5 +465,8 @@ return {
|
|
|
376
465
|
ATTR_HASH = ATTR_HASH,
|
|
377
466
|
ATTR_VERSION = ATTR_VERSION,
|
|
378
467
|
ATTR_PATH = ATTR_PATH,
|
|
468
|
+
BACKUP_NAME = BACKUP_NAME,
|
|
469
|
+
ATTR_BACKUP_PLACE_VERSION = ATTR_BACKUP_PLACE_VERSION,
|
|
470
|
+
ATTR_BACKUP_SAVED_AS = ATTR_BACKUP_SAVED_AS,
|
|
379
471
|
MAX_KEY_LENGTH = MAX_KEY_LENGTH,
|
|
380
472
|
}
|
package/out/assets/sync.luau
CHANGED
|
@@ -4,6 +4,7 @@ local _services = TS.import(script, TS.getModule(script, "@rbxts", "services"))
|
|
|
4
4
|
local CollectionService = _services.CollectionService
|
|
5
5
|
local HttpService = _services.HttpService
|
|
6
6
|
local InsertService = _services.InsertService
|
|
7
|
+
local ServerStorage = _services.ServerStorage
|
|
7
8
|
local _dom = TS.import(script, script.Parent, "dom")
|
|
8
9
|
local liveCopies = _dom.liveCopies
|
|
9
10
|
local numberAttribute = _dom.numberAttribute
|
|
@@ -15,10 +16,14 @@ local ATTR_HASH = _manifest.ATTR_HASH
|
|
|
15
16
|
local ATTR_ID = _manifest.ATTR_ID
|
|
16
17
|
local ATTR_KEY = _manifest.ATTR_KEY
|
|
17
18
|
local ATTR_PATH = _manifest.ATTR_PATH
|
|
19
|
+
local ATTR_BACKUP_PLACE_VERSION = _manifest.ATTR_BACKUP_PLACE_VERSION
|
|
20
|
+
local ATTR_BACKUP_SAVED_AS = _manifest.ATTR_BACKUP_SAVED_AS
|
|
18
21
|
local ATTR_VERSION = _manifest.ATTR_VERSION
|
|
22
|
+
local BACKUP_NAME = _manifest.BACKUP_NAME
|
|
19
23
|
local decideAsset = _manifest.decideAsset
|
|
20
24
|
local keyOfTag = _manifest.keyOfTag
|
|
21
25
|
local parseManifest = _manifest.parseManifest
|
|
26
|
+
local placeMatchesManifest = _manifest.placeMatchesManifest
|
|
22
27
|
local summarizeAssets = _manifest.summarizeAssets
|
|
23
28
|
--[[
|
|
24
29
|
*
|
|
@@ -217,6 +222,17 @@ local function readManifest(kernel, deadline)
|
|
|
217
222
|
from = "payload",
|
|
218
223
|
}
|
|
219
224
|
end
|
|
225
|
+
--* What the place's backup build says about its hot-asset copies (BackupVouch), if anything.
|
|
226
|
+
local function backupVouch()
|
|
227
|
+
local backup = ServerStorage:FindFirstChild(BACKUP_NAME)
|
|
228
|
+
if backup == nil then
|
|
229
|
+
return nil
|
|
230
|
+
end
|
|
231
|
+
return {
|
|
232
|
+
placeVersion = numberAttribute(backup, ATTR_BACKUP_PLACE_VERSION),
|
|
233
|
+
savedAs = numberAttribute(backup, ATTR_BACKUP_SAVED_AS),
|
|
234
|
+
}
|
|
235
|
+
end
|
|
220
236
|
--* Marks `instance` as the live copy of `entry` (attributes first, so they replicate with the tag).
|
|
221
237
|
local function stamp(instance, entry, version)
|
|
222
238
|
instance:SetAttribute(ATTR_KEY, entry.key)
|
|
@@ -283,7 +299,7 @@ local function loadCopy(entry)
|
|
|
283
299
|
container:Destroy()
|
|
284
300
|
local removed = stripScripts(root)
|
|
285
301
|
if removed > 0 then
|
|
286
|
-
warn("[src/assets/sync.ts:
|
|
302
|
+
warn("[src/assets/sync.ts:215]", `hot asset {entry.key}: removed {removed} scripts`)
|
|
287
303
|
end
|
|
288
304
|
return root
|
|
289
305
|
end
|
|
@@ -409,7 +425,7 @@ local function syncEntry(entry, status, placeMatches, ownEpoch, threads)
|
|
|
409
425
|
if _condition == nil then
|
|
410
426
|
_condition = entry.ver
|
|
411
427
|
end
|
|
412
|
-
warn("[src/assets/sync.ts:
|
|
428
|
+
warn("[src/assets/sync.ts:298]", `hot asset {_exp_1} (version {_condition}) failed: {reason}`)
|
|
413
429
|
return nil
|
|
414
430
|
end
|
|
415
431
|
-- The new copy is live: the copies it replaces go.
|
|
@@ -422,7 +438,7 @@ local function syncEntry(entry, status, placeMatches, ownEpoch, threads)
|
|
|
422
438
|
status.ms = math.floor((os.clock() - started) * 1000)
|
|
423
439
|
if late then
|
|
424
440
|
status.late = true
|
|
425
|
-
print("[src/assets/sync.ts:
|
|
441
|
+
print("[src/assets/sync.ts:311]", `hot asset {entry.key} loaded late ({status.ms} ms)`)
|
|
426
442
|
end
|
|
427
443
|
end)
|
|
428
444
|
table.insert(_threads, _arg0)
|
|
@@ -471,7 +487,16 @@ local function run(kernel, deadline, ownEpoch, threads)
|
|
|
471
487
|
end
|
|
472
488
|
setManifest(manifest)
|
|
473
489
|
report.manifest = "ok"
|
|
474
|
-
|
|
490
|
+
-- A publish that changed nothing outside the kernel slots (the CLI's backup refresh) doesn't move servers to the
|
|
491
|
+
-- upload path: the backup vouches for the version it saved (2026-10-13 incident).
|
|
492
|
+
local place = placeMatchesManifest(manifest.placeVersion, game.PlaceVersion, backupVouch())
|
|
493
|
+
report.place = place
|
|
494
|
+
report.manifestPlaceVersion = manifest.placeVersion
|
|
495
|
+
report.placeVersion = game.PlaceVersion
|
|
496
|
+
local placeMatches = place == "same" or place == "vouched"
|
|
497
|
+
if place == "differs" then
|
|
498
|
+
warn("[src/assets/sync.ts:356]", `hot assets: the manifest is for place v{manifest.placeVersion}, this server runs v{game.PlaceVersion}: keys without a matching baked copy load from their uploaded versions (typetorch assets sync --deploy <branch> fixes it)`)
|
|
499
|
+
end
|
|
475
500
|
local loads = {}
|
|
476
501
|
for _, entry in manifest.entries do
|
|
477
502
|
local status = {
|
|
@@ -490,7 +515,7 @@ local function run(kernel, deadline, ownEpoch, threads)
|
|
|
490
515
|
if not ok then
|
|
491
516
|
status.source = "failed"
|
|
492
517
|
status.error = shortError(result)
|
|
493
|
-
warn("[src/assets/sync.ts:
|
|
518
|
+
warn("[src/assets/sync.ts:369]", `hot asset {entry.key} failed: {status.error}`)
|
|
494
519
|
elseif result ~= nil then
|
|
495
520
|
table.insert(loads, result)
|
|
496
521
|
end
|
|
@@ -524,7 +549,7 @@ local function run(kernel, deadline, ownEpoch, threads)
|
|
|
524
549
|
end
|
|
525
550
|
report.timedOut = true
|
|
526
551
|
load.fail("timed out")
|
|
527
|
-
warn("[src/assets/sync.ts:
|
|
552
|
+
warn("[src/assets/sync.ts:378]", `hot asset {load.status.key}: still loading; the old copy stays until it finishes`)
|
|
528
553
|
end
|
|
529
554
|
report.unmanaged = unmanagedKeys()
|
|
530
555
|
end
|
|
@@ -571,7 +596,7 @@ local function syncHotAssets(kernel, trove)
|
|
|
571
596
|
local _errors = report.errors
|
|
572
597
|
local _arg0 = `AssetSync failed: {shortError(err)}`
|
|
573
598
|
table.insert(_errors, _arg0)
|
|
574
|
-
warn("[src/assets/sync.ts:
|
|
599
|
+
warn("[src/assets/sync.ts:407]", `AssetSync failed: {err}`)
|
|
575
600
|
end
|
|
576
601
|
report.running = false
|
|
577
602
|
report.ms = math.floor((os.clock() - started) * 1000)
|
|
@@ -592,7 +617,7 @@ local function syncHotAssets(kernel, trove)
|
|
|
592
617
|
table.insert(parts, _arg0)
|
|
593
618
|
end
|
|
594
619
|
table.sort(parts)
|
|
595
|
-
print("[src/assets/sync.ts:
|
|
620
|
+
print("[src/assets/sync.ts:417]", `hot assets: {table.concat(parts, ", ")} in {report.ms} ms (place {report.place or "?"})`)
|
|
596
621
|
end
|
|
597
622
|
end
|
|
598
623
|
return {
|
|
@@ -63,6 +63,19 @@ local function drawServer(target, report)
|
|
|
63
63
|
target:field("Manifest", if ok then `{#report.entries} keys{if report.from ~= nil then ` ({report.from})` else ""}` else report.manifest, if ok then COLORS.text else COLORS.warn)
|
|
64
64
|
local sync = if report.running then "Running" elseif report.ms ~= nil then `{report.ms} ms{if report.timedOut == true then " timed out" else ""}` else "-"
|
|
65
65
|
target:field("Sync", sync, if report.timedOut == true then COLORS.warn else COLORS.text)
|
|
66
|
+
if report.place ~= nil then
|
|
67
|
+
local _condition = report.manifestPlaceVersion
|
|
68
|
+
if _condition == nil then
|
|
69
|
+
_condition = "?"
|
|
70
|
+
end
|
|
71
|
+
local _condition_1 = report.placeVersion
|
|
72
|
+
if _condition_1 == nil then
|
|
73
|
+
_condition_1 = "?"
|
|
74
|
+
end
|
|
75
|
+
local versions = `manifest v{_condition}, place v{_condition_1}`
|
|
76
|
+
local place = if report.place == "differs" then `differs ({versions}): keys load from uploads` elseif report.place == "vouched" then `vouched by the backup ({versions})` else report.place
|
|
77
|
+
target:field("Place", place, if report.place == "differs" then COLORS.warn else COLORS.text)
|
|
78
|
+
end
|
|
66
79
|
for _, why in report.errors do
|
|
67
80
|
target:text(why, COLORS.warn)
|
|
68
81
|
end
|
|
@@ -188,7 +201,7 @@ local function renderAssetsTab(tab, options)
|
|
|
188
201
|
end
|
|
189
202
|
end)
|
|
190
203
|
if not ok then
|
|
191
|
-
warn("[src/devtools/assets-ui.ts:
|
|
204
|
+
warn("[src/devtools/assets-ui.ts:143]", `[devtools] assets refresh failed: {err}`)
|
|
192
205
|
end
|
|
193
206
|
task.wait(REFRESH)
|
|
194
207
|
end
|
package/out/devtools/client.d.ts
CHANGED
|
@@ -5,9 +5,4 @@ export interface DevtoolsClient {
|
|
|
5
5
|
/** Call once the client generation is up (all modules started). */
|
|
6
6
|
readonly started: () => void;
|
|
7
7
|
}
|
|
8
|
-
/**
|
|
9
|
-
* Starts the dev menu for this client generation. Everything it creates or connects lives in `trove`, so a swap
|
|
10
|
-
* removes it; the open state, tab, sub-tab, window rectangle and options survive the swap through the kernel persist
|
|
11
|
-
* store.
|
|
12
|
-
*/
|
|
13
8
|
export declare function startDevtoolsClient(kernel: ClientKernel, dispatcher: ClientDispatcher, trove: Trove): DevtoolsClient;
|
package/out/devtools/client.luau
CHANGED
|
@@ -467,6 +467,52 @@ local function shareableLogs(entries)
|
|
|
467
467
|
return ordered
|
|
468
468
|
end
|
|
469
469
|
-- The menu ----------------------------------------------------------------------------------------------------------
|
|
470
|
+
--[[
|
|
471
|
+
*
|
|
472
|
+
* The channel in a Builds group heading: the branch's own channel, not its newest artifact's. A promoted prod artifact
|
|
473
|
+
* keeps channel "prod" on a dev branch, and the heading used to say "(prod)" until the next dev deploy flipped it back.
|
|
474
|
+
* This server's branch: `branchChannel` from its status (kernel 0.3.9); another branch: the branch list's
|
|
475
|
+
* `branchChannel` (kernel 0.5.2), else its `channel`. "dev, prod artifact" when the newest artifact differs.
|
|
476
|
+
|
|
477
|
+
]]
|
|
478
|
+
local function buildsHeadingChannel(branchName, artifactChannel, branches, status)
|
|
479
|
+
local channel
|
|
480
|
+
if status ~= nil and status.branch == branchName and status.branchChannel ~= nil then
|
|
481
|
+
channel = status.branchChannel
|
|
482
|
+
end
|
|
483
|
+
if channel == nil then
|
|
484
|
+
-- ▼ ReadonlyArray.find ▼
|
|
485
|
+
local _callback = function(branch)
|
|
486
|
+
return branch.name == branchName
|
|
487
|
+
end
|
|
488
|
+
local _result
|
|
489
|
+
for _i, _v in branches do
|
|
490
|
+
if _callback(_v, _i - 1, branches) == true then
|
|
491
|
+
_result = _v
|
|
492
|
+
break
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
-- ▲ ReadonlyArray.find ▲
|
|
496
|
+
local info = _result
|
|
497
|
+
local _result_1 = info
|
|
498
|
+
if _result_1 ~= nil then
|
|
499
|
+
_result_1 = _result_1.branchChannel
|
|
500
|
+
end
|
|
501
|
+
local _condition = _result_1
|
|
502
|
+
if _condition == nil then
|
|
503
|
+
local _result_2 = info
|
|
504
|
+
if _result_2 ~= nil then
|
|
505
|
+
_result_2 = _result_2.channel
|
|
506
|
+
end
|
|
507
|
+
_condition = _result_2
|
|
508
|
+
end
|
|
509
|
+
channel = _condition
|
|
510
|
+
end
|
|
511
|
+
if channel == nil or channel == artifactChannel then
|
|
512
|
+
return artifactChannel
|
|
513
|
+
end
|
|
514
|
+
return `{channel}, {artifactChannel} artifact`
|
|
515
|
+
end
|
|
470
516
|
--[[
|
|
471
517
|
*
|
|
472
518
|
* Kernel 0.3.9: "dev" for a dev branch, plus the rules when they are stricter ("dev (prod rules)": a dev branch on a
|
|
@@ -488,7 +534,36 @@ end
|
|
|
488
534
|
* store.
|
|
489
535
|
|
|
490
536
|
]]
|
|
537
|
+
local DEV_GUI_NAME = "TypeTorchDev"
|
|
538
|
+
--* The client generation that built a dev menu gui (an attribute on the ScreenGui).
|
|
539
|
+
local DEV_GUI_GENERATION = "TypeTorchGeneration"
|
|
540
|
+
--[[
|
|
541
|
+
*
|
|
542
|
+
* Destroys dev menus an older generation left behind. Its trove should have removed them, but a generation whose stop
|
|
543
|
+
* was cut short (a throwing cleanup, before framework 0.5.2) left its menu on screen, dead, over the new one.
|
|
544
|
+
* Menus without the attribute come from a framework before 0.5.2: always older, since this generation hasn't built one.
|
|
545
|
+
|
|
546
|
+
]]
|
|
547
|
+
local function sweepStaleMenus(playerGui, generation)
|
|
548
|
+
for _, child in playerGui:GetChildren() do
|
|
549
|
+
if child.Name ~= DEV_GUI_NAME or not child:IsA("ScreenGui") then
|
|
550
|
+
continue
|
|
551
|
+
end
|
|
552
|
+
local built = child:GetAttribute(DEV_GUI_GENERATION)
|
|
553
|
+
if type(built) == "number" and built >= generation then
|
|
554
|
+
continue
|
|
555
|
+
end
|
|
556
|
+
pcall(function()
|
|
557
|
+
return child:Destroy()
|
|
558
|
+
end)
|
|
559
|
+
end
|
|
560
|
+
end
|
|
491
561
|
local function startDevtoolsClient(kernel, dispatcher, trove)
|
|
562
|
+
-- Before anything else, so a leaked menu goes away even for players who never get the dev menu.
|
|
563
|
+
local startGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
|
|
564
|
+
if startGui then
|
|
565
|
+
sweepStaleMenus(startGui, kernel.generation)
|
|
566
|
+
end
|
|
492
567
|
local state = kernel:persist(PERSIST_KEY, function()
|
|
493
568
|
return {
|
|
494
569
|
open = false,
|
|
@@ -664,7 +739,7 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
|
|
|
664
739
|
-- picks the file up on its PC).
|
|
665
740
|
local _devGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
|
|
666
741
|
if _devGui ~= nil then
|
|
667
|
-
_devGui = _devGui:FindFirstChild(
|
|
742
|
+
_devGui = _devGui:FindFirstChild(DEV_GUI_NAME)
|
|
668
743
|
end
|
|
669
744
|
local devGui = _devGui
|
|
670
745
|
local taken = takeScreenshot(if devGui ~= nil and devGui:IsA("ScreenGui") then { devGui } else {})
|
|
@@ -2080,7 +2155,7 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
|
|
|
2080
2155
|
end
|
|
2081
2156
|
for _, branchName in order do
|
|
2082
2157
|
local group = groups[branchName]
|
|
2083
|
-
local heading = body:text(`{branchName} ({group[1].channel})`, COLORS.text)
|
|
2158
|
+
local heading = body:text(`{branchName} ({buildsHeadingChannel(branchName, group[1].channel, data.branches, status)})`, COLORS.text)
|
|
2084
2159
|
heading.Font = Enum.Font.BuilderSansBold
|
|
2085
2160
|
local showAll = expanded[branchName] ~= nil
|
|
2086
2161
|
-- ▼ ReadonlyArray.forEach ▼
|
|
@@ -2472,7 +2547,7 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
|
|
|
2472
2547
|
})
|
|
2473
2548
|
end)
|
|
2474
2549
|
if not ok then
|
|
2475
|
-
warn("[src/devtools/client.ts:
|
|
2550
|
+
warn("[src/devtools/client.ts:1768]", `[devtools] {key} tab failed: {err}`)
|
|
2476
2551
|
page:text(`This tab failed: {err}`, COLORS.bad)
|
|
2477
2552
|
end
|
|
2478
2553
|
end
|
|
@@ -2543,13 +2618,15 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
|
|
|
2543
2618
|
local open
|
|
2544
2619
|
local build = function()
|
|
2545
2620
|
local playerGui = Players.LocalPlayer:WaitForChild("PlayerGui")
|
|
2621
|
+
sweepStaleMenus(playerGui, kernel.generation)
|
|
2546
2622
|
local gui = trove:add(make("ScreenGui", {
|
|
2547
|
-
Name =
|
|
2623
|
+
Name = DEV_GUI_NAME,
|
|
2548
2624
|
ResetOnSpawn = false,
|
|
2549
2625
|
DisplayOrder = DEV_MENU_DISPLAY_ORDER,
|
|
2550
2626
|
IgnoreGuiInset = true,
|
|
2551
2627
|
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
|
2552
2628
|
}))
|
|
2629
|
+
gui:SetAttribute(DEV_GUI_GENERATION, kernel.generation)
|
|
2553
2630
|
local toggle = style(make("TextButton", {
|
|
2554
2631
|
Name = "Toggle",
|
|
2555
2632
|
AutoButtonColor = true,
|
|
@@ -3077,7 +3154,7 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
|
|
|
3077
3154
|
return playReloadSound()
|
|
3078
3155
|
end)
|
|
3079
3156
|
if not ok then
|
|
3080
|
-
warn("[src/devtools/client.ts:
|
|
3157
|
+
warn("[src/devtools/client.ts:2189]", `[devtools] reload sound failed: {err}`)
|
|
3081
3158
|
end
|
|
3082
3159
|
end))
|
|
3083
3160
|
end,
|
package/out/devtools/health.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { AnalyticsStats } from "../analytics/schema";
|
|
|
2
2
|
import type { AssetFacts } from "../assets/manifest";
|
|
3
3
|
import type { DetachedStatus, ErrorReportsStatus, FleetSenderInfo, HealthInfo, KernelStatus, PerfSummary, SettingsStatus } from "../kernel";
|
|
4
4
|
/** The newest kernel this framework release knows about. Bump it with every kernel release. */
|
|
5
|
-
export declare const LATEST_KERNEL = "0.
|
|
5
|
+
export declare const LATEST_KERNEL = "0.5.3";
|
|
6
6
|
/**
|
|
7
7
|
* The live health window while it is open (kernel 0.3.7 thresholds; older kernels: 3): "1/3 errors, 24 s left", plus
|
|
8
8
|
* ", no rollback" when a failure would only mark the server degraded. Undefined once the window closed.
|
package/out/devtools/health.luau
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
local TS = _G[script]
|
|
3
3
|
local describeFleetError = TS.import(script, script.Parent.Parent, "analytics", "hints").describeFleetError
|
|
4
4
|
--* The newest kernel this framework release knows about. Bump it with every kernel release.
|
|
5
|
-
local LATEST_KERNEL = "0.
|
|
5
|
+
local LATEST_KERNEL = "0.5.3"
|
|
6
6
|
--* Kernels before 0.3.7 use fixed thresholds: 3 errors within 30 s of ready roll back.
|
|
7
7
|
local OLD_HEALTH_ERRORS = 3
|
|
8
8
|
local OLD_HEALTH_WINDOW = 30
|
package/out/kernel.d.ts
CHANGED
|
@@ -353,7 +353,10 @@ export interface ArtifactEntry {
|
|
|
353
353
|
}
|
|
354
354
|
export interface BranchInfo {
|
|
355
355
|
name: string;
|
|
356
|
+
/** The head artifact's channel when the settings don't configure the branch (a promoted prod build: "prod"). */
|
|
356
357
|
channel: Channel;
|
|
358
|
+
/** Kernel 0.5.2: what the branch is ("prod": the default branch or one configured prod; else "dev"). */
|
|
359
|
+
branchChannel?: Channel;
|
|
357
360
|
artifactId?: string;
|
|
358
361
|
/** Kernel 0.2+. */
|
|
359
362
|
assetId?: number;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Trove } from "@rbxts/trove";
|
|
2
|
+
/**
|
|
3
|
+
* `trove.clean()`, but every object is cleaned in its own pcall: a cleanup that throws warns (`<label> cleanup threw`)
|
|
4
|
+
* and the rest still run. `trove.clean()` stops at the first error and leaves the trove stuck "cleaning", so one
|
|
5
|
+
* `trove.remove(x)` inside a cleanup ("Cannot call trove.remove() while cleaning") used to abort the whole generation
|
|
6
|
+
* stop: later modules' troves, the network and the dev menu stayed alive. Child troves (`trove.extend()`) are swept the
|
|
7
|
+
* same way. Falls back to a pcalled `trove.clean()` for a trove it doesn't recognize.
|
|
8
|
+
*/
|
|
9
|
+
export declare function cleanTrove(trove: Trove, label: string): void;
|
|
10
|
+
/** Runs one step of a generation stop; a step that throws warns and the stop goes on. */
|
|
11
|
+
export declare function stopStep(label: string, step: () => void): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
--* The parts of @rbxts/trove 1.x that `trove.clean()` uses; read so one throwing cleanup can't skip the rest.
|
|
3
|
+
--[[
|
|
4
|
+
*
|
|
5
|
+
* `trove.clean()`, but every object is cleaned in its own pcall: a cleanup that throws warns (`<label> cleanup threw`)
|
|
6
|
+
* and the rest still run. `trove.clean()` stops at the first error and leaves the trove stuck "cleaning", so one
|
|
7
|
+
* `trove.remove(x)` inside a cleanup ("Cannot call trove.remove() while cleaning") used to abort the whole generation
|
|
8
|
+
* stop: later modules' troves, the network and the dev menu stayed alive. Child troves (`trove.extend()`) are swept the
|
|
9
|
+
* same way. Falls back to a pcalled `trove.clean()` for a trove it doesn't recognize.
|
|
10
|
+
|
|
11
|
+
]]
|
|
12
|
+
local function cleanTrove(trove, label)
|
|
13
|
+
local internals = trove
|
|
14
|
+
local objects = internals.objects
|
|
15
|
+
local cleanupObj = internals.cleanupObj
|
|
16
|
+
if not (type(objects) == "table") or not (type(cleanupObj) == "function") then
|
|
17
|
+
local ok, err = pcall(function()
|
|
18
|
+
return trove:clean()
|
|
19
|
+
end)
|
|
20
|
+
if not ok then
|
|
21
|
+
warn("[src/runtime/safe-clean.ts:28]", `{label} cleanup threw: {err}`)
|
|
22
|
+
end
|
|
23
|
+
return nil
|
|
24
|
+
end
|
|
25
|
+
-- Already being cleaned further up the stack (a cleanup that cleans its own trove): leave it to that clean.
|
|
26
|
+
if internals.cleaning == true then
|
|
27
|
+
return nil
|
|
28
|
+
end
|
|
29
|
+
internals.cleaning = true
|
|
30
|
+
local troveClass = getmetatable(trove)
|
|
31
|
+
for _, track in objects do
|
|
32
|
+
local obj = track.obj
|
|
33
|
+
local ok, err = pcall(function()
|
|
34
|
+
if type(obj) == "table" and getmetatable(obj) == troveClass then
|
|
35
|
+
cleanTrove(obj, label)
|
|
36
|
+
else
|
|
37
|
+
cleanupObj(internals, track)
|
|
38
|
+
end
|
|
39
|
+
end)
|
|
40
|
+
if not ok then
|
|
41
|
+
warn("[src/runtime/safe-clean.ts:41]", `{label} cleanup threw: {err}`)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
table.clear(objects)
|
|
45
|
+
internals.cleaning = false
|
|
46
|
+
end
|
|
47
|
+
--* Runs one step of a generation stop; a step that throws warns and the stop goes on.
|
|
48
|
+
local function stopStep(label, step)
|
|
49
|
+
local ok, err = pcall(step)
|
|
50
|
+
if not ok then
|
|
51
|
+
warn("[src/runtime/safe-clean.ts:50]", `TypeTorch stop: {label} threw: {err}`)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
return {
|
|
55
|
+
cleanTrove = cleanTrove,
|
|
56
|
+
stopStep = stopStep,
|
|
57
|
+
}
|
package/out/runtime/start.luau
CHANGED
|
@@ -36,6 +36,9 @@ local _registry = TS.import(script, script.Parent, "registry")
|
|
|
36
36
|
local persistKeys = _registry.persistKeys
|
|
37
37
|
local registered = _registry.registered
|
|
38
38
|
local runningModules = _registry.runningModules
|
|
39
|
+
local _safe_clean = TS.import(script, script.Parent, "safe-clean")
|
|
40
|
+
local cleanTrove = _safe_clean.cleanTrove
|
|
41
|
+
local stopStep = _safe_clean.stopStep
|
|
39
42
|
local function has(instance, method)
|
|
40
43
|
local _arg0 = instance[method]
|
|
41
44
|
return type(_arg0) == "function"
|
|
@@ -305,14 +308,29 @@ local function start(realm, kernel, options)
|
|
|
305
308
|
return (running.instance):onStop()
|
|
306
309
|
end)
|
|
307
310
|
if not ok then
|
|
308
|
-
warn("[src/runtime/start.ts:
|
|
311
|
+
warn("[src/runtime/start.ts:206]", `{running.name}.onStop threw: {err}`)
|
|
309
312
|
end
|
|
310
313
|
end
|
|
311
|
-
|
|
314
|
+
-- Object by object, each pcalled: a cleanup that throws (e.g. trove.remove inside a cleanup) can't skip the
|
|
315
|
+
-- rest of this module, the modules after it, or the rest of the stop.
|
|
316
|
+
cleanTrove(running.trove, running.name)
|
|
317
|
+
stopStep(`{running.name} trove`, function()
|
|
318
|
+
return root:remove(running.trove)
|
|
319
|
+
end)
|
|
312
320
|
end
|
|
313
321
|
table.clear(runningModules)
|
|
314
322
|
setModulePhase("stopped")
|
|
315
323
|
end
|
|
324
|
+
--* Every step runs even when an earlier one throws (each warns), so a bad cleanup can't keep a dead generation alive.
|
|
325
|
+
local stopGeneration = function()
|
|
326
|
+
stopStep("stopping modules", stopModules)
|
|
327
|
+
stopStep("stopping the network", stopNetwork)
|
|
328
|
+
stopStep("unbinding TypeTorch", unbindTypeTorch)
|
|
329
|
+
cleanTrove(root, "TypeTorch generation")
|
|
330
|
+
stopStep("destroying the generation trove", function()
|
|
331
|
+
return root:destroy()
|
|
332
|
+
end)
|
|
333
|
+
end
|
|
316
334
|
setModulePhase("constructing")
|
|
317
335
|
local initialized, initError = pcall(function()
|
|
318
336
|
for _, mod in ordered do
|
|
@@ -377,10 +395,7 @@ local function start(realm, kernel, options)
|
|
|
377
395
|
end
|
|
378
396
|
end)
|
|
379
397
|
if not initialized then
|
|
380
|
-
|
|
381
|
-
stopNetwork()
|
|
382
|
-
unbindTypeTorch()
|
|
383
|
-
root:destroy()
|
|
398
|
+
stopGeneration()
|
|
384
399
|
error(`TypeTorch {realm} failed to start: {initError}`, 0)
|
|
385
400
|
end
|
|
386
401
|
for _, running in runningModules do
|
|
@@ -446,14 +461,13 @@ local function start(realm, kernel, options)
|
|
|
446
461
|
_result()
|
|
447
462
|
end
|
|
448
463
|
startedTypeTorch()
|
|
449
|
-
print("[src/runtime/start.ts:
|
|
464
|
+
print("[src/runtime/start.ts:305]", `TypeTorch {realm} started {#runningModules} modules in {math.floor((os.clock() - startedAt) * 1000)} ms (artifact {kernel.artifact.id}, generation {kernel.generation})`)
|
|
450
465
|
return function(info)
|
|
451
466
|
-- TypeTorch.onSwapOut first, while every module still runs (so they can save into persist).
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
root:destroy()
|
|
467
|
+
stopStep("onSwapOut", function()
|
|
468
|
+
return swapOutTypeTorch(info)
|
|
469
|
+
end)
|
|
470
|
+
stopGeneration()
|
|
457
471
|
end
|
|
458
472
|
end
|
|
459
473
|
--* Call from the game's `src/server/boot.ts`: `export function boot(kernel) { return startServer(kernel, {...}) }`.
|
package/out/settings.luau
CHANGED
|
@@ -168,13 +168,14 @@ do
|
|
|
168
168
|
if _result == "client" then
|
|
169
169
|
error(`TypeTorch.liveConfig("{self.key}") is server-only`, 2)
|
|
170
170
|
end
|
|
171
|
+
-- `typetorch test --cloud` runs a stub kernel with no signed settings at all: the default is expected there.
|
|
171
172
|
local _result_1 = current
|
|
172
173
|
if _result_1 ~= nil then
|
|
173
174
|
_result_1 = _result_1.kernel
|
|
174
175
|
end
|
|
175
176
|
local _condition = _result_1 ~= nil
|
|
176
177
|
if _condition then
|
|
177
|
-
_condition = not settingsSupported()
|
|
178
|
+
_condition = current.kernel.test ~= true and not settingsSupported()
|
|
178
179
|
end
|
|
179
180
|
if _condition then
|
|
180
181
|
warnOnce(`old:{self.key}`, `TypeTorch.liveConfig("{self.key}") needs kernel {SETTINGS_KERNEL} (signed settings; this server runs {current.kernelVersion}): using the default`)
|
package/out/version.d.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* This framework's version, as package.json says (scripts/test-state-inspect.luau checks they match). Shown in
|
|
3
3
|
* uploaded log headers (Logs > Upload); bump it with package.json.
|
|
4
4
|
*/
|
|
5
|
-
export declare const FRAMEWORK_VERSION = "0.5.
|
|
5
|
+
export declare const FRAMEWORK_VERSION = "0.5.3";
|
package/out/version.luau
CHANGED
package/package.json
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@typetorch/framework",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "TypeTorch framework for roblox-ts: hot-swappable modules (dependency injection, lifecycle, troves), guarded networking, hot assets and the in-game dev menu. Ships inside every TypeTorch artifact.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "TypeTorch",
|
|
7
|
-
"homepage": "https://github.com/typetorch/framework#readme",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/typetorch/framework.git"
|
|
11
|
-
},
|
|
12
|
-
"bugs": {
|
|
13
|
-
"url": "https://github.com/typetorch/framework/issues"
|
|
14
|
-
},
|
|
15
|
-
"keywords": ["roblox", "roblox-ts", "rbxts", "hot-reload", "hot-swap", "dependency-injection", "typetorch"],
|
|
16
|
-
"main": "out/init.luau",
|
|
17
|
-
"types": "out/index.d.ts",
|
|
18
|
-
"files": ["out", "!**/*.tsbuildinfo"],
|
|
19
|
-
"publishConfig": {
|
|
20
|
-
"access": "public"
|
|
21
|
-
},
|
|
22
|
-
"scripts": {
|
|
23
|
-
"build": "rbxtsc --type package",
|
|
24
|
-
"watch": "rbxtsc --type package -w",
|
|
25
|
-
"clean": "node -e \"require('fs').rmSync('out',{recursive:true,force:true})\"",
|
|
26
|
-
"prepublishOnly": "node -e \"require('fs').rmSync('out',{recursive:true,force:true})\" && rbxtsc --type package"
|
|
27
|
-
},
|
|
28
|
-
"peerDependencies": {
|
|
29
|
-
"@rbxts/compiler-types": "^3.0.0-types.0",
|
|
30
|
-
"@typetorch/transformer": "^0.2.0",
|
|
31
|
-
"roblox-ts": "^3.0.0"
|
|
32
|
-
},
|
|
33
|
-
"peerDependenciesMeta": {
|
|
34
|
-
"@typetorch/transformer": {
|
|
35
|
-
"optional": true
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
"dependencies": {
|
|
39
|
-
"@rbxts/services": "^1.6.0",
|
|
40
|
-
"@rbxts/t": "^3.2.1",
|
|
41
|
-
"@rbxts/trove": "^1.3.0"
|
|
42
|
-
},
|
|
43
|
-
"devDependencies": {
|
|
44
|
-
"@rbxts/compiler-types": "^3.0.0-types.0",
|
|
45
|
-
"@rbxts/types": "^1.0.956",
|
|
46
|
-
"rbxts-transform-debug": "2.2.0",
|
|
47
|
-
"roblox-ts": "^3.0.0",
|
|
48
|
-
"typescript": "5.5.3"
|
|
49
|
-
},
|
|
50
|
-
"overrides": {
|
|
51
|
-
"typescript": "5.5.3"
|
|
52
|
-
}
|
|
53
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@typetorch/framework",
|
|
3
|
+
"version": "0.5.3",
|
|
4
|
+
"description": "TypeTorch framework for roblox-ts: hot-swappable modules (dependency injection, lifecycle, troves), guarded networking, hot assets and the in-game dev menu. Ships inside every TypeTorch artifact.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "TypeTorch",
|
|
7
|
+
"homepage": "https://github.com/typetorch/framework#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/typetorch/framework.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/typetorch/framework/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": ["roblox", "roblox-ts", "rbxts", "hot-reload", "hot-swap", "dependency-injection", "typetorch"],
|
|
16
|
+
"main": "out/init.luau",
|
|
17
|
+
"types": "out/index.d.ts",
|
|
18
|
+
"files": ["out", "!**/*.tsbuildinfo"],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "rbxtsc --type package",
|
|
24
|
+
"watch": "rbxtsc --type package -w",
|
|
25
|
+
"clean": "node -e \"require('fs').rmSync('out',{recursive:true,force:true})\"",
|
|
26
|
+
"prepublishOnly": "node -e \"require('fs').rmSync('out',{recursive:true,force:true})\" && rbxtsc --type package"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@rbxts/compiler-types": "^3.0.0-types.0",
|
|
30
|
+
"@typetorch/transformer": "^0.2.0",
|
|
31
|
+
"roblox-ts": "^3.0.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"@typetorch/transformer": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@rbxts/services": "^1.6.0",
|
|
40
|
+
"@rbxts/t": "^3.2.1",
|
|
41
|
+
"@rbxts/trove": "^1.3.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@rbxts/compiler-types": "^3.0.0-types.0",
|
|
45
|
+
"@rbxts/types": "^1.0.956",
|
|
46
|
+
"rbxts-transform-debug": "2.2.0",
|
|
47
|
+
"roblox-ts": "^3.0.0",
|
|
48
|
+
"typescript": "5.5.3"
|
|
49
|
+
},
|
|
50
|
+
"overrides": {
|
|
51
|
+
"typescript": "5.5.3"
|
|
52
|
+
}
|
|
53
|
+
}
|