@typetorch/framework 0.5.2 → 0.5.4
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/out/analytics/SCHEMA.md +1 -0
- package/out/analytics/server.luau +22 -6
- package/out/assets/dom.d.ts +8 -0
- package/out/assets/dom.luau +64 -0
- package/out/assets/manifest.d.ts +31 -0
- package/out/assets/manifest.luau +92 -0
- package/out/assets/sync.luau +78 -8
- package/out/devtools/assets-ui.luau +14 -1
- package/out/devtools/health.d.ts +1 -1
- package/out/devtools/health.luau +1 -1
- package/out/version.d.ts +1 -1
- package/out/version.luau +1 -1
- package/package.json +1 -1
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 `""`) |
|
|
@@ -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
|
|
@@ -495,7 +497,7 @@ do
|
|
|
495
497
|
end
|
|
496
498
|
function ServerAnalytics:track(player, kind, name, props)
|
|
497
499
|
if not isEventName(name) then
|
|
498
|
-
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)}`)
|
|
499
501
|
return nil
|
|
500
502
|
end
|
|
501
503
|
local session = if player then self:sessionOf(player) else nil
|
|
@@ -603,7 +605,7 @@ do
|
|
|
603
605
|
end
|
|
604
606
|
local control = _condition
|
|
605
607
|
if not isExperimentName(name) or not isVariantList(variants) then
|
|
606
|
-
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)`)
|
|
607
609
|
return control
|
|
608
610
|
end
|
|
609
611
|
local session = self:sessionOf(player)
|
|
@@ -668,7 +670,7 @@ do
|
|
|
668
670
|
function ServerAnalytics:applySettings(parsed)
|
|
669
671
|
local errorsText = table.concat(parsed.errors, "; ")
|
|
670
672
|
if errorsText ~= table.concat(self.settingsErrors, "; ") and errorsText ~= "" then
|
|
671
|
-
warn("[src/analytics/server.ts:
|
|
673
|
+
warn("[src/analytics/server.ts:560]", `[analytics] the sink settings (settings.backend or settings.analytics): {errorsText}`)
|
|
672
674
|
end
|
|
673
675
|
self.settingsErrors = parsed.errors
|
|
674
676
|
local hadSettings = self.settings ~= nil
|
|
@@ -698,7 +700,7 @@ do
|
|
|
698
700
|
-- `new AnalyticsEngine({ settings })`.
|
|
699
701
|
if not settingsSupported() then
|
|
700
702
|
if self.kernel ~= nil and self.kernel.test ~= true then
|
|
701
|
-
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 \})`)
|
|
702
704
|
end
|
|
703
705
|
self.settingsTried = true
|
|
704
706
|
return nil
|
|
@@ -1227,6 +1229,20 @@ do
|
|
|
1227
1229
|
load = start.loadSeconds,
|
|
1228
1230
|
}))
|
|
1229
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
|
|
1230
1246
|
end
|
|
1231
1247
|
self.trove:add(TypeTorch:onSwapOut(function(info)
|
|
1232
1248
|
local _self = self
|
|
@@ -1500,7 +1516,7 @@ do
|
|
|
1500
1516
|
end
|
|
1501
1517
|
end)
|
|
1502
1518
|
if not ok then
|
|
1503
|
-
warn("[src/analytics/server.ts:
|
|
1519
|
+
warn("[src/analytics/server.ts:1086]", `[analytics] client message failed: {string.sub(tostring(err), 1, 200)}`)
|
|
1504
1520
|
end
|
|
1505
1521
|
end)
|
|
1506
1522
|
self.trove:add(function()
|
|
@@ -1783,7 +1799,7 @@ do
|
|
|
1783
1799
|
self.lastWarnAt = now
|
|
1784
1800
|
local more = if self.suppressed > 0 then ` (+{self.suppressed} more in the last minute)` else ""
|
|
1785
1801
|
self.suppressed = 0
|
|
1786
|
-
warn("[src/analytics/server.ts:
|
|
1802
|
+
warn("[src/analytics/server.ts:1281]", `[analytics] {message}{more}`)
|
|
1787
1803
|
return true
|
|
1788
1804
|
end
|
|
1789
1805
|
function ServerAnalytics:noteSuccess()
|
package/out/assets/dom.d.ts
CHANGED
|
@@ -13,5 +13,13 @@ export declare function resolveParent(path: string, create: boolean): Instance |
|
|
|
13
13
|
* cloning a UI template into PlayerGui) keep the tag and the attributes but live elsewhere, so they never count.
|
|
14
14
|
*/
|
|
15
15
|
export declare function isLiveCopy(instance: Instance): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Copies the CollectionService tags of `from` and its descendants onto the instances at the same place in `to` (matched
|
|
18
|
+
* by name, and by order among siblings of the same name); AssetSync's own `__typetorch_asset:` tags stay out. A loaded
|
|
19
|
+
* hot-asset version arrives without the tags its place copy has (2026-10-13: a game that finds its UI by tag lost it
|
|
20
|
+
* on every server that loaded the uploads), so the load gives them back from the place's copy before it goes live.
|
|
21
|
+
* Returns how many tags were added.
|
|
22
|
+
*/
|
|
23
|
+
export declare function restoreTags(from: Instance, to: Instance): number;
|
|
16
24
|
/** The live copies tagged for `key` (normally one; two for a moment while a new version replaces the old). */
|
|
17
25
|
export declare function liveCopies(key: string): Instance[];
|
package/out/assets/dom.luau
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
local TS = _G[script]
|
|
3
3
|
local CollectionService = TS.import(script, TS.getModule(script, "@rbxts", "services")).CollectionService
|
|
4
4
|
local _manifest = TS.import(script, script.Parent, "manifest")
|
|
5
|
+
local ASSET_TAG_PREFIX = _manifest.ASSET_TAG_PREFIX
|
|
5
6
|
local assetTag = _manifest.assetTag
|
|
6
7
|
local ATTR_PATH = _manifest.ATTR_PATH
|
|
7
8
|
local pathSegments = _manifest.pathSegments
|
|
@@ -73,6 +74,68 @@ local function isLiveCopy(instance)
|
|
|
73
74
|
local parent = instance.Parent
|
|
74
75
|
return path ~= nil and parent ~= nil and resolveParent(path, false) == parent
|
|
75
76
|
end
|
|
77
|
+
--[[
|
|
78
|
+
*
|
|
79
|
+
* Copies the CollectionService tags of `from` and its descendants onto the instances at the same place in `to` (matched
|
|
80
|
+
* by name, and by order among siblings of the same name); AssetSync's own `__typetorch_asset:` tags stay out. A loaded
|
|
81
|
+
* hot-asset version arrives without the tags its place copy has (2026-10-13: a game that finds its UI by tag lost it
|
|
82
|
+
* on every server that loaded the uploads), so the load gives them back from the place's copy before it goes live.
|
|
83
|
+
* Returns how many tags were added.
|
|
84
|
+
|
|
85
|
+
]]
|
|
86
|
+
local function restoreTags(from, to)
|
|
87
|
+
local added = 0
|
|
88
|
+
local visit
|
|
89
|
+
visit = function(source, target)
|
|
90
|
+
for _, tag in source:GetTags() do
|
|
91
|
+
local _arg1 = #ASSET_TAG_PREFIX
|
|
92
|
+
local _condition = string.sub(tag, 1, _arg1) == ASSET_TAG_PREFIX
|
|
93
|
+
if not _condition then
|
|
94
|
+
_condition = target:HasTag(tag)
|
|
95
|
+
end
|
|
96
|
+
if _condition then
|
|
97
|
+
continue
|
|
98
|
+
end
|
|
99
|
+
target:AddTag(tag)
|
|
100
|
+
added += 1
|
|
101
|
+
end
|
|
102
|
+
local byName = {}
|
|
103
|
+
for _, child in target:GetChildren() do
|
|
104
|
+
local _name = child.Name
|
|
105
|
+
local list = byName[_name]
|
|
106
|
+
if list == nil then
|
|
107
|
+
local _name_1 = child.Name
|
|
108
|
+
local _arg1 = { child }
|
|
109
|
+
byName[_name_1] = _arg1
|
|
110
|
+
else
|
|
111
|
+
table.insert(list, child)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
local seen = {}
|
|
115
|
+
for _, child in source:GetChildren() do
|
|
116
|
+
local _name = child.Name
|
|
117
|
+
local _condition = seen[_name]
|
|
118
|
+
if _condition == nil then
|
|
119
|
+
_condition = 0
|
|
120
|
+
end
|
|
121
|
+
local index = _condition
|
|
122
|
+
local _name_1 = child.Name
|
|
123
|
+
local _arg1 = index + 1
|
|
124
|
+
seen[_name_1] = _arg1
|
|
125
|
+
local _name_2 = child.Name
|
|
126
|
+
local _match = byName[_name_2]
|
|
127
|
+
if _match ~= nil then
|
|
128
|
+
_match = _match[index + 1]
|
|
129
|
+
end
|
|
130
|
+
local match = _match
|
|
131
|
+
if match ~= nil then
|
|
132
|
+
visit(child, match)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
visit(from, to)
|
|
137
|
+
return added
|
|
138
|
+
end
|
|
76
139
|
--* The live copies tagged for `key` (normally one; two for a moment while a new version replaces the old).
|
|
77
140
|
local function liveCopies(key)
|
|
78
141
|
local _exp = CollectionService:GetTagged(assetTag(key))
|
|
@@ -93,5 +156,6 @@ return {
|
|
|
93
156
|
stringAttribute = stringAttribute,
|
|
94
157
|
resolveParent = resolveParent,
|
|
95
158
|
isLiveCopy = isLiveCopy,
|
|
159
|
+
restoreTags = restoreTags,
|
|
96
160
|
liveCopies = liveCopies,
|
|
97
161
|
}
|
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);
|
|
@@ -122,12 +144,19 @@ export interface AssetStatus {
|
|
|
122
144
|
ms?: number;
|
|
123
145
|
/** Loaded after the start-up wait ran out. */
|
|
124
146
|
late?: boolean;
|
|
147
|
+
/** CollectionService tags a loaded copy got back from the place's copy (restoreTags). */
|
|
148
|
+
tags?: number;
|
|
125
149
|
}
|
|
126
150
|
export interface AssetSyncReport {
|
|
127
151
|
/** none: the artifact has no Assets attribute; invalid: unusable; unread: reading it failed (see errors). */
|
|
128
152
|
manifest: "none" | "ok" | "invalid" | "unread";
|
|
129
153
|
/** Where the manifest was found: "mount", "Server" (the payload's Server folder) or "payload" (re-read). */
|
|
130
154
|
from?: string;
|
|
155
|
+
/** placeMatchesManifest: whether the place's copies are the manifest's ("differs": the upload path). */
|
|
156
|
+
place?: PlaceMatch;
|
|
157
|
+
/** The manifest's placeVersion and this server's game.PlaceVersion. */
|
|
158
|
+
manifestPlaceVersion?: number;
|
|
159
|
+
placeVersion?: number;
|
|
131
160
|
errors: string[];
|
|
132
161
|
/** AssetSync is still holding the generation's start. */
|
|
133
162
|
running: boolean;
|
|
@@ -138,6 +167,8 @@ export interface AssetSyncReport {
|
|
|
138
167
|
/** Tagged keys this manifest doesn't name: left as they are. */
|
|
139
168
|
unmanaged: string[];
|
|
140
169
|
}
|
|
170
|
+
/** Keys per source, and the rest of what the `tech` / `assets` analytics row carries (SCHEMA.md). */
|
|
171
|
+
export declare function assetCounts(report: AssetSyncReport): Record<string, unknown>;
|
|
141
172
|
/** What Server > Status needs (devtools ServerFacts.assets). */
|
|
142
173
|
export interface AssetFacts {
|
|
143
174
|
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,10 +4,12 @@ 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
|
|
10
11
|
local resolveParent = _dom.resolveParent
|
|
12
|
+
local restoreTags = _dom.restoreTags
|
|
11
13
|
local stringAttribute = _dom.stringAttribute
|
|
12
14
|
local _manifest = TS.import(script, script.Parent, "manifest")
|
|
13
15
|
local assetTag = _manifest.assetTag
|
|
@@ -15,10 +17,14 @@ local ATTR_HASH = _manifest.ATTR_HASH
|
|
|
15
17
|
local ATTR_ID = _manifest.ATTR_ID
|
|
16
18
|
local ATTR_KEY = _manifest.ATTR_KEY
|
|
17
19
|
local ATTR_PATH = _manifest.ATTR_PATH
|
|
20
|
+
local ATTR_BACKUP_PLACE_VERSION = _manifest.ATTR_BACKUP_PLACE_VERSION
|
|
21
|
+
local ATTR_BACKUP_SAVED_AS = _manifest.ATTR_BACKUP_SAVED_AS
|
|
18
22
|
local ATTR_VERSION = _manifest.ATTR_VERSION
|
|
23
|
+
local BACKUP_NAME = _manifest.BACKUP_NAME
|
|
19
24
|
local decideAsset = _manifest.decideAsset
|
|
20
25
|
local keyOfTag = _manifest.keyOfTag
|
|
21
26
|
local parseManifest = _manifest.parseManifest
|
|
27
|
+
local placeMatchesManifest = _manifest.placeMatchesManifest
|
|
22
28
|
local summarizeAssets = _manifest.summarizeAssets
|
|
23
29
|
--[[
|
|
24
30
|
*
|
|
@@ -217,6 +223,17 @@ local function readManifest(kernel, deadline)
|
|
|
217
223
|
from = "payload",
|
|
218
224
|
}
|
|
219
225
|
end
|
|
226
|
+
--* What the place's backup build says about its hot-asset copies (BackupVouch), if anything.
|
|
227
|
+
local function backupVouch()
|
|
228
|
+
local backup = ServerStorage:FindFirstChild(BACKUP_NAME)
|
|
229
|
+
if backup == nil then
|
|
230
|
+
return nil
|
|
231
|
+
end
|
|
232
|
+
return {
|
|
233
|
+
placeVersion = numberAttribute(backup, ATTR_BACKUP_PLACE_VERSION),
|
|
234
|
+
savedAs = numberAttribute(backup, ATTR_BACKUP_SAVED_AS),
|
|
235
|
+
}
|
|
236
|
+
end
|
|
220
237
|
--* Marks `instance` as the live copy of `entry` (attributes first, so they replicate with the tag).
|
|
221
238
|
local function stamp(instance, entry, version)
|
|
222
239
|
instance:SetAttribute(ATTR_KEY, entry.key)
|
|
@@ -283,7 +300,7 @@ local function loadCopy(entry)
|
|
|
283
300
|
container:Destroy()
|
|
284
301
|
local removed = stripScripts(root)
|
|
285
302
|
if removed > 0 then
|
|
286
|
-
warn("[src/assets/sync.ts:
|
|
303
|
+
warn("[src/assets/sync.ts:215]", `hot asset {entry.key}: removed {removed} scripts`)
|
|
287
304
|
end
|
|
288
305
|
return root
|
|
289
306
|
end
|
|
@@ -393,6 +410,50 @@ local function syncEntry(entry, status, placeMatches, ownEpoch, threads)
|
|
|
393
410
|
if target == nil then
|
|
394
411
|
error(`no {string.split(entry.path, "/")[1]} service`, 0)
|
|
395
412
|
end
|
|
413
|
+
-- The uploaded version has no CollectionService tags: the place's copy (on a hot swap, the copy being
|
|
414
|
+
-- replaced, which got them the same way) gives them back (restoreTags).
|
|
415
|
+
local _result_1
|
|
416
|
+
if fallback ~= nil and fallback.Parent ~= nil then
|
|
417
|
+
_result_1 = fallback
|
|
418
|
+
else
|
|
419
|
+
-- ▼ ReadonlyArray.find ▼
|
|
420
|
+
local _callback_2 = function(candidate)
|
|
421
|
+
return candidate.Parent ~= nil
|
|
422
|
+
end
|
|
423
|
+
local _result_2
|
|
424
|
+
for _i, _v in baked do
|
|
425
|
+
if _callback_2(_v, _i - 1, baked) == true then
|
|
426
|
+
_result_2 = _v
|
|
427
|
+
break
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
-- ▲ ReadonlyArray.find ▲
|
|
431
|
+
_result_1 = _result_2
|
|
432
|
+
end
|
|
433
|
+
local source = _result_1
|
|
434
|
+
do
|
|
435
|
+
local index = #tagged - 1
|
|
436
|
+
local _shouldIncrement = false
|
|
437
|
+
while true do
|
|
438
|
+
if _shouldIncrement then
|
|
439
|
+
index -= 1
|
|
440
|
+
else
|
|
441
|
+
_shouldIncrement = true
|
|
442
|
+
end
|
|
443
|
+
if not (source == nil and index >= 0) then
|
|
444
|
+
break
|
|
445
|
+
end
|
|
446
|
+
if tagged[index + 1].Parent ~= nil then
|
|
447
|
+
source = tagged[index + 1]
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
if source ~= nil then
|
|
452
|
+
local restored = restoreTags(source, copy)
|
|
453
|
+
if restored > 0 then
|
|
454
|
+
status.tags = restored
|
|
455
|
+
end
|
|
456
|
+
end
|
|
396
457
|
stamp(copy, entry, entry.ver)
|
|
397
458
|
copy.Parent = target
|
|
398
459
|
end)
|
|
@@ -409,7 +470,7 @@ local function syncEntry(entry, status, placeMatches, ownEpoch, threads)
|
|
|
409
470
|
if _condition == nil then
|
|
410
471
|
_condition = entry.ver
|
|
411
472
|
end
|
|
412
|
-
warn("[src/assets/sync.ts:
|
|
473
|
+
warn("[src/assets/sync.ts:308]", `hot asset {_exp_1} (version {_condition}) failed: {reason}`)
|
|
413
474
|
return nil
|
|
414
475
|
end
|
|
415
476
|
-- The new copy is live: the copies it replaces go.
|
|
@@ -422,7 +483,7 @@ local function syncEntry(entry, status, placeMatches, ownEpoch, threads)
|
|
|
422
483
|
status.ms = math.floor((os.clock() - started) * 1000)
|
|
423
484
|
if late then
|
|
424
485
|
status.late = true
|
|
425
|
-
print("[src/assets/sync.ts:
|
|
486
|
+
print("[src/assets/sync.ts:321]", `hot asset {entry.key} loaded late ({status.ms} ms)`)
|
|
426
487
|
end
|
|
427
488
|
end)
|
|
428
489
|
table.insert(_threads, _arg0)
|
|
@@ -471,7 +532,16 @@ local function run(kernel, deadline, ownEpoch, threads)
|
|
|
471
532
|
end
|
|
472
533
|
setManifest(manifest)
|
|
473
534
|
report.manifest = "ok"
|
|
474
|
-
|
|
535
|
+
-- A publish that changed nothing outside the kernel slots (the CLI's backup refresh) doesn't move servers to the
|
|
536
|
+
-- upload path: the backup vouches for the version it saved (2026-10-13 incident).
|
|
537
|
+
local place = placeMatchesManifest(manifest.placeVersion, game.PlaceVersion, backupVouch())
|
|
538
|
+
report.place = place
|
|
539
|
+
report.manifestPlaceVersion = manifest.placeVersion
|
|
540
|
+
report.placeVersion = game.PlaceVersion
|
|
541
|
+
local placeMatches = place == "same" or place == "vouched"
|
|
542
|
+
if place == "differs" then
|
|
543
|
+
warn("[src/assets/sync.ts:366]", `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)`)
|
|
544
|
+
end
|
|
475
545
|
local loads = {}
|
|
476
546
|
for _, entry in manifest.entries do
|
|
477
547
|
local status = {
|
|
@@ -490,7 +560,7 @@ local function run(kernel, deadline, ownEpoch, threads)
|
|
|
490
560
|
if not ok then
|
|
491
561
|
status.source = "failed"
|
|
492
562
|
status.error = shortError(result)
|
|
493
|
-
warn("[src/assets/sync.ts:
|
|
563
|
+
warn("[src/assets/sync.ts:379]", `hot asset {entry.key} failed: {status.error}`)
|
|
494
564
|
elseif result ~= nil then
|
|
495
565
|
table.insert(loads, result)
|
|
496
566
|
end
|
|
@@ -524,7 +594,7 @@ local function run(kernel, deadline, ownEpoch, threads)
|
|
|
524
594
|
end
|
|
525
595
|
report.timedOut = true
|
|
526
596
|
load.fail("timed out")
|
|
527
|
-
warn("[src/assets/sync.ts:
|
|
597
|
+
warn("[src/assets/sync.ts:388]", `hot asset {load.status.key}: still loading; the old copy stays until it finishes`)
|
|
528
598
|
end
|
|
529
599
|
report.unmanaged = unmanagedKeys()
|
|
530
600
|
end
|
|
@@ -571,7 +641,7 @@ local function syncHotAssets(kernel, trove)
|
|
|
571
641
|
local _errors = report.errors
|
|
572
642
|
local _arg0 = `AssetSync failed: {shortError(err)}`
|
|
573
643
|
table.insert(_errors, _arg0)
|
|
574
|
-
warn("[src/assets/sync.ts:
|
|
644
|
+
warn("[src/assets/sync.ts:417]", `AssetSync failed: {err}`)
|
|
575
645
|
end
|
|
576
646
|
report.running = false
|
|
577
647
|
report.ms = math.floor((os.clock() - started) * 1000)
|
|
@@ -592,7 +662,7 @@ local function syncHotAssets(kernel, trove)
|
|
|
592
662
|
table.insert(parts, _arg0)
|
|
593
663
|
end
|
|
594
664
|
table.sort(parts)
|
|
595
|
-
print("[src/assets/sync.ts:
|
|
665
|
+
print("[src/assets/sync.ts:427]", `hot assets: {table.concat(parts, ", ")} in {report.ms} ms (place {report.place or "?"})`)
|
|
596
666
|
end
|
|
597
667
|
end
|
|
598
668
|
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/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.
|
|
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.
|
|
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/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.4";
|
package/out/version.luau
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typetorch/framework",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"author": "TypeTorch",
|