@trioforge/crucible 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/maths/spline/package.json +4 -0
  2. package/out/client/index.d.ts +7 -0
  3. package/out/client/init.luau +36 -1
  4. package/out/configs.d.ts +37 -0
  5. package/out/configs.luau +46 -0
  6. package/out/data.d.ts +224 -0
  7. package/out/data.luau +97 -0
  8. package/out/emit/configs.d.ts +4 -0
  9. package/out/emit/configs.luau +22 -0
  10. package/out/emit/data.d.ts +6 -0
  11. package/out/emit/data.luau +19 -0
  12. package/out/emit/index.d.ts +5 -1
  13. package/out/emit/init.luau +23 -7
  14. package/out/emit/network.d.ts +6 -0
  15. package/out/emit/network.luau +36 -0
  16. package/out/emit/replicated.d.ts +4 -0
  17. package/out/emit/replicated.luau +13 -0
  18. package/out/index.d.ts +8 -0
  19. package/out/init.luau +23 -0
  20. package/out/internal/binding.luau +5 -0
  21. package/out/internal/boot.luau +112 -8
  22. package/out/internal/configs/limits.d.ts +10 -0
  23. package/out/internal/configs/limits.luau +25 -0
  24. package/out/internal/configs/port.d.ts +29 -0
  25. package/out/internal/configs/port.luau +99 -0
  26. package/out/internal/configs/runtime.d.ts +21 -0
  27. package/out/internal/configs/runtime.luau +981 -0
  28. package/out/internal/data/admission.d.ts +1 -0
  29. package/out/internal/data/admission.luau +673 -0
  30. package/out/internal/data/declarations.d.ts +1 -0
  31. package/out/internal/data/declarations.luau +384 -0
  32. package/out/internal/data/envelope.d.ts +1 -0
  33. package/out/internal/data/envelope.luau +318 -0
  34. package/out/internal/data/handles.d.ts +1 -0
  35. package/out/internal/data/handles.luau +255 -0
  36. package/out/internal/data/inbox-transforms.d.ts +1 -0
  37. package/out/internal/data/inbox-transforms.luau +458 -0
  38. package/out/internal/data/inbox.d.ts +1 -0
  39. package/out/internal/data/inbox.luau +437 -0
  40. package/out/internal/data/lease.d.ts +1 -0
  41. package/out/internal/data/lease.luau +304 -0
  42. package/out/internal/data/limits.d.ts +1 -0
  43. package/out/internal/data/limits.luau +234 -0
  44. package/out/internal/data/messages.d.ts +1 -0
  45. package/out/internal/data/messages.luau +69 -0
  46. package/out/internal/data/operation-id.d.ts +1 -0
  47. package/out/internal/data/operation-id.luau +61 -0
  48. package/out/internal/data/paths.d.ts +2 -0
  49. package/out/internal/data/paths.luau +323 -0
  50. package/out/internal/data/port.d.ts +1 -0
  51. package/out/internal/data/port.luau +290 -0
  52. package/out/internal/data/profiles.d.ts +1 -0
  53. package/out/internal/data/profiles.luau +862 -0
  54. package/out/internal/data/receipts.d.ts +1 -0
  55. package/out/internal/data/receipts.luau +190 -0
  56. package/out/internal/data/record-bindings.d.ts +1 -0
  57. package/out/internal/data/record-bindings.luau +92 -0
  58. package/out/internal/data/record-declarations.d.ts +1 -0
  59. package/out/internal/data/record-declarations.luau +140 -0
  60. package/out/internal/data/records.d.ts +1 -0
  61. package/out/internal/data/records.luau +400 -0
  62. package/out/internal/data/runtime.d.ts +1 -0
  63. package/out/internal/data/runtime.luau +918 -0
  64. package/out/internal/data/scheduler.d.ts +1 -0
  65. package/out/internal/data/scheduler.luau +507 -0
  66. package/out/internal/data/session.d.ts +1 -0
  67. package/out/internal/data/session.luau +234 -0
  68. package/out/internal/data/shutdown.d.ts +1 -0
  69. package/out/internal/data/shutdown.luau +91 -0
  70. package/out/internal/data/waiters.d.ts +1 -0
  71. package/out/internal/data/waiters.luau +65 -0
  72. package/out/internal/lifecycle.luau +45 -0
  73. package/out/internal/network/catalog.d.ts +26 -0
  74. package/out/internal/network/catalog.luau +182 -0
  75. package/out/internal/network/claims.d.ts +1 -0
  76. package/out/internal/network/claims.luau +146 -0
  77. package/out/internal/network/client-session.d.ts +1 -0
  78. package/out/internal/network/client-session.luau +405 -0
  79. package/out/internal/network/codec.d.ts +63 -0
  80. package/out/internal/network/codec.luau +1183 -0
  81. package/out/internal/network/envelope.d.ts +1 -0
  82. package/out/internal/network/envelope.luau +247 -0
  83. package/out/internal/network/limits.d.ts +26 -0
  84. package/out/internal/network/limits.luau +55 -0
  85. package/out/internal/network/port.d.ts +1 -0
  86. package/out/internal/network/port.luau +19 -0
  87. package/out/internal/network/roblox-network-port.d.ts +1 -0
  88. package/out/internal/network/roblox-network-port.luau +105 -0
  89. package/out/internal/network/schema.d.ts +72 -0
  90. package/out/internal/network/schema.luau +386 -0
  91. package/out/internal/network/server-session.d.ts +1 -0
  92. package/out/internal/network/server-session.luau +395 -0
  93. package/out/internal/network/session.d.ts +1 -0
  94. package/out/internal/network/session.luau +266 -0
  95. package/out/internal/reactive/context.d.ts +1 -0
  96. package/out/internal/reactive/context.luau +149 -0
  97. package/out/internal/reactive/flush.d.ts +1 -0
  98. package/out/internal/reactive/flush.luau +355 -0
  99. package/out/internal/reactive/graph.d.ts +1 -0
  100. package/out/internal/reactive/graph.luau +626 -0
  101. package/out/internal/reactive/limits.d.ts +1 -0
  102. package/out/internal/reactive/limits.luau +54 -0
  103. package/out/internal/reactive/ownership.d.ts +1 -0
  104. package/out/internal/reactive/ownership.luau +626 -0
  105. package/out/internal/registry.luau +10 -1
  106. package/out/internal/sync/client-sync.d.ts +1 -0
  107. package/out/internal/sync/client-sync.luau +157 -0
  108. package/out/internal/sync/declarations.d.ts +1 -0
  109. package/out/internal/sync/declarations.luau +276 -0
  110. package/out/internal/sync/frame.d.ts +1 -0
  111. package/out/internal/sync/frame.luau +91 -0
  112. package/out/internal/sync/server-sync.d.ts +1 -0
  113. package/out/internal/sync/server-sync.luau +502 -0
  114. package/out/internal/sync/values.d.ts +1 -0
  115. package/out/internal/sync/values.luau +930 -0
  116. package/out/maths/spline/curve.d.ts +35 -0
  117. package/out/maths/spline/curve.luau +1122 -0
  118. package/out/maths/spline/index.d.ts +2 -0
  119. package/out/maths/spline/init.luau +8 -0
  120. package/out/network.d.ts +66 -0
  121. package/out/network.luau +24 -0
  122. package/out/reactive.d.ts +76 -0
  123. package/out/reactive.luau +136 -0
  124. package/out/replicated.d.ts +38 -0
  125. package/out/replicated.luau +45 -0
  126. package/out/server/data.d.ts +54 -0
  127. package/out/server/data.luau +44 -0
  128. package/out/server/index.d.ts +17 -0
  129. package/out/server/init.luau +58 -4
  130. package/out/server/publish.d.ts +20 -0
  131. package/out/server/publish.luau +24 -0
  132. package/out/testing/index.d.ts +38 -0
  133. package/out/testing/init.luau +189 -0
  134. package/out/util/random/index.d.ts +24 -0
  135. package/out/util/random/init.luau +301 -0
  136. package/out/vide/index.d.ts +7 -0
  137. package/out/vide/init.luau +50 -0
  138. package/package.json +28 -2
  139. package/testing/package.json +4 -0
  140. package/transformer/data.cjs +72 -0
  141. package/transformer/declarations.cjs +153 -0
  142. package/transformer/index.js +87 -23
  143. package/transformer/network.cjs +961 -0
  144. package/util/random/package.json +4 -0
  145. package/vide/package.json +4 -0
@@ -0,0 +1,384 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _schema = TS.import(script, script.Parent.Parent, "network", "schema")
4
+ local assertDescriptor = _schema.assertDescriptor
5
+ local n = _schema.n
6
+ local readState = TS.import(script, script.Parent.Parent, "reactive", "graph").readState
7
+ local MAX_REPLICATED_DECLARATIONS = TS.import(script, script.Parent.Parent, "reactive", "limits").MAX_REPLICATED_DECLARATIONS
8
+ local _declarations = TS.import(script, script.Parent.Parent, "sync", "declarations")
9
+ local declarationOf = _declarations.declarationOf
10
+ local declareFrameworkReplicated = _declarations.declareFrameworkReplicated
11
+ local replicatedCapacity = _declarations.replicatedCapacity
12
+ local _values = TS.import(script, script.Parent.Parent, "sync", "values")
13
+ local admitReplicated = _values.admit
14
+ local compilePlan = _values.compilePlan
15
+ local sealSchema = TS.import(script, script.Parent, "admission").sealSchema
16
+ local rootHandle = TS.import(script, script.Parent, "handles").rootHandle
17
+ local _limits = TS.import(script, script.Parent, "limits")
18
+ local LIVE_NAME_PREFIX = _limits.LIVE_NAME_PREFIX
19
+ local MAX_DATA_DECLARATIONS = _limits.MAX_DATA_DECLARATIONS
20
+ local MAX_PROFILE_STORES = _limits.MAX_PROFILE_STORES
21
+ local VIEW_NAME_MAX_BYTES = _limits.VIEW_NAME_MAX_BYTES
22
+ local VIEW_NAME_PREFIX = _limits.VIEW_NAME_PREFIX
23
+ local _paths = TS.import(script, script.Parent, "paths")
24
+ local fieldPaths = _paths.fieldPaths
25
+ local getPath = _paths.getPath
26
+ local pathLess = _paths.pathLess
27
+ local samePath = _paths.samePath
28
+ local showPath = _paths.showPath
29
+ local _record_declarations = TS.import(script, script.Parent, "record-declarations")
30
+ local checkStoreName = _record_declarations.checkStoreName
31
+ local dataDeclarationCount = _record_declarations.dataDeclarationCount
32
+ local dataRegistrySealed = _record_declarations.dataRegistrySealed
33
+ local registerProfileName = _record_declarations.registerProfileName
34
+ local storeNameTaken = _record_declarations.storeNameTaken
35
+ --* @internal One top-level field the owner's client receives, and its view declaration (rules 125, 134).
36
+ --* @internal A profile store as both realms know it, built once at module evaluation (rule 4).
37
+ --* @internal What the server entry installs so a shared declaration reaches the durability engine (rule 115).
38
+ --[[
39
+ *
40
+ * The record a store carries in its metatable. The key is a string, not a private table, so `replicatedPaths` reads a
41
+ * declaration made by another module set (rule 131).
42
+
43
+ ]]
44
+ local RECORD = "__crucibleProfileStore"
45
+ local profiles = {}
46
+ local clientRoots = {}
47
+ local serverSide
48
+ local markMeta = {}
49
+ --* @internal `serverOnly(value)`: a frozen mark recognized by its private metatable (rule 129).
50
+ local function markServerOnly(value)
51
+ return table.freeze(setmetatable({
52
+ value = value,
53
+ }, markMeta))
54
+ end
55
+ local function isMark(value)
56
+ local _value = value
57
+ local _condition = type(_value) == "table"
58
+ if _condition then
59
+ _condition = getmetatable(value) == markMeta
60
+ end
61
+ return _condition
62
+ end
63
+ --* Replaces every mark by the value it wraps, recording where each one sat as an array of keys (rule 4).
64
+ local function unwrap(value, path, found)
65
+ if isMark(value) then
66
+ local _found = found
67
+ local _path = path
68
+ table.insert(_found, _path)
69
+ return unwrap(value.value, path, found)
70
+ end
71
+ local _value = value
72
+ local _condition = not (type(_value) == "table")
73
+ if not _condition then
74
+ _condition = getmetatable(value) ~= nil
75
+ end
76
+ if _condition then
77
+ return value
78
+ end
79
+ local copy
80
+ for key, entry in pairs(value) do
81
+ local _array = {}
82
+ local _length = #_array
83
+ local _pathLength = #path
84
+ table.move(path, 1, _pathLength, _length + 1, _array)
85
+ _length += _pathLength
86
+ _array[_length + 1] = tostring(key)
87
+ local inner = unwrap(entry, _array, found)
88
+ if rawequal(inner, entry) then
89
+ continue
90
+ end
91
+ if copy == nil then
92
+ copy = table.clone(value)
93
+ end
94
+ copy[key] = inner
95
+ end
96
+ return copy or value
97
+ end
98
+ local function checkMarks(name, found, expected)
99
+ local _array = {}
100
+ local _length = #_array
101
+ table.move(found, 1, #found, _length + 1, _array)
102
+ table.sort(_array, pathLess)
103
+ local marked = _array
104
+ local _array_1 = {}
105
+ local _length_1 = #_array_1
106
+ table.move(expected, 1, #expected, _length_1 + 1, _array_1)
107
+ table.sort(_array_1, pathLess)
108
+ local typed = _array_1
109
+ for _, path in marked do
110
+ -- ▼ ReadonlyArray.some ▼
111
+ local _result = false
112
+ local _callback = function(other)
113
+ return samePath(other, path)
114
+ end
115
+ for _k, _v in typed do
116
+ if _callback(_v, _k - 1, typed) then
117
+ _result = true
118
+ break
119
+ end
120
+ end
121
+ -- ▲ ReadonlyArray.some ▲
122
+ if not _result then
123
+ error(`{name} {showPath(path)} is marked serverOnly in its template value but not in its type`, 0)
124
+ end
125
+ end
126
+ for _, path in typed do
127
+ -- ▼ ReadonlyArray.some ▼
128
+ local _result = false
129
+ local _callback = function(other)
130
+ return samePath(other, path)
131
+ end
132
+ for _k, _v in marked do
133
+ if _callback(_v, _k - 1, marked) then
134
+ _result = true
135
+ break
136
+ end
137
+ end
138
+ -- ▲ ReadonlyArray.some ▲
139
+ if not _result then
140
+ error(`{name} {showPath(path)} is serverOnly in its type but its template value is not marked`, 0)
141
+ end
142
+ end
143
+ end
144
+ --* A field view's descriptor: the field's replica node admitting undefined (rule 125).
145
+ local function admittingUndefined(node)
146
+ if node.kind == "union" then
147
+ for _, branch in node.branches do
148
+ if branch.kind == "nil" then
149
+ return node
150
+ end
151
+ end
152
+ local _array = { n["nil"] }
153
+ local _length = #_array
154
+ local _array_1 = node.branches
155
+ table.move(_array_1, 1, #_array_1, _length + 1, _array)
156
+ return n.union(_array)
157
+ end
158
+ return n.union({ n["nil"], node })
159
+ end
160
+ --* Rules 2 and 134: every view name is checked, and the realm's room counted, before any is declared.
161
+ local function declareViews(name, replica)
162
+ local prefix = `{VIEW_NAME_PREFIX}{name}.`
163
+ for _, _binding in replica.fields do
164
+ local field = _binding[1]
165
+ if (string.match(field, "^[A-Za-z][A-Za-z0-9_]*$")) == nil then
166
+ error(`{name}["{field}"]: a field the owner's client receives is named by one segment, [A-Za-z][A-Za-z0-9_]*; rename it or mark it serverOnly`, 0)
167
+ end
168
+ local view = prefix .. field
169
+ if #view > VIEW_NAME_MAX_BYTES then
170
+ error(`{name}.{field}: its view name {view} is {#view} bytes, over {VIEW_NAME_MAX_BYTES}; this store allows field names of at most {VIEW_NAME_MAX_BYTES - #prefix} bytes`, 0)
171
+ end
172
+ end
173
+ local needed = 1 + #replica.fields
174
+ local room = replicatedCapacity()
175
+ if room < needed then
176
+ error(`{name} needs {needed} replicated declarations; {MAX_REPLICATED_DECLARATIONS - room} of {MAX_REPLICATED_DECLARATIONS} are in use`, 0)
177
+ end
178
+ local live = declareFrameworkReplicated(`{LIVE_NAME_PREFIX}{name}`, false, n.bool)
179
+ local _exp = replica.fields
180
+ -- ▼ ReadonlyArray.map ▼
181
+ local _newValue = table.create(#_exp)
182
+ local _callback = function(_param)
183
+ local field = _param[1]
184
+ local node = _param[2]
185
+ local handle = declareFrameworkReplicated(prefix .. field, nil, admittingUndefined(node))
186
+ return {
187
+ name = field,
188
+ handle = handle,
189
+ declaration = declarationOf(handle),
190
+ }
191
+ end
192
+ for _k, _v in _exp do
193
+ _newValue[_k] = _callback(_v, _k - 1, _exp)
194
+ end
195
+ -- ▲ ReadonlyArray.map ▲
196
+ local fields = _newValue
197
+ return {
198
+ live = live,
199
+ fields = fields,
200
+ }
201
+ end
202
+ local function clientSource(declaration)
203
+ local byName = {}
204
+ for _, field in declaration.fields do
205
+ local _name = field.name
206
+ byName[_name] = field
207
+ end
208
+ return {
209
+ name = declaration.name,
210
+ client = true,
211
+ beforeRead = function(self) end,
212
+ write = function(self)
213
+ error(`{declaration.name}() is read-only on the client`, 3)
214
+ end,
215
+ read = function(self, path)
216
+ local live = readState(declaration.liveDeclaration.node) == true
217
+ if #path == 0 then
218
+ if not live then
219
+ return declaration.clientTemplate
220
+ end
221
+ local root = {}
222
+ for _, field in declaration.fields do
223
+ root[field.name] = readState(field.declaration.node)
224
+ end
225
+ return table.freeze(root)
226
+ end
227
+ local _arg0 = path[1]
228
+ local field = byName[_arg0]
229
+ if live and field ~= nil then
230
+ return getPath(readState(field.declaration.node), path, 1)
231
+ end
232
+ return getPath(declaration.clientTemplate, path)
233
+ end,
234
+ }
235
+ end
236
+ --* @internal Rule 124: the local player's root handle, made once per realm.
237
+ local function clientRoot(declaration)
238
+ local _declaration = declaration
239
+ local root = clientRoots[_declaration]
240
+ if root == nil then
241
+ root = rootHandle(clientSource(declaration), declaration.replica)
242
+ local _declaration_1 = declaration
243
+ local _root = root
244
+ clientRoots[_declaration_1] = _root
245
+ end
246
+ return root
247
+ end
248
+ --* @internal Rule 126: tracked; the lifecycle is ready and true, and every field view is ready.
249
+ local function clientLoaded(declaration)
250
+ local live = declaration.liveDeclaration
251
+ if readState(live.status) ~= "ready" or readState(live.node) ~= true then
252
+ return false
253
+ end
254
+ for _, field in declaration.fields do
255
+ if readState(field.declaration.status) ~= "ready" then
256
+ return false
257
+ end
258
+ end
259
+ return true
260
+ end
261
+ local function storeMeta(declaration)
262
+ local name = declaration.name
263
+ local meta = {
264
+ __call = function(_, ...)
265
+ local args = { ... }
266
+ local player = args[1]
267
+ if serverSide ~= nil then
268
+ if player == nil then
269
+ error(`{name} is per player on the server; call {name}(player)`, 2)
270
+ end
271
+ return serverSide.root(declaration, player)
272
+ end
273
+ if #args > 0 then
274
+ error(`a client reads only its own profile; call {name}()`, 2)
275
+ end
276
+ return clientRoot(declaration)
277
+ end,
278
+ __index = function(_, key)
279
+ if serverSide ~= nil then
280
+ return serverSide.member(declaration, key)
281
+ end
282
+ if key == "loaded" then
283
+ return function(_store, player)
284
+ if player ~= nil then
285
+ error(`{name}.loaded(player) is server-only; the client reads {name}.loaded()`, 2)
286
+ end
287
+ return clientLoaded(declaration)
288
+ end
289
+ end
290
+ return function()
291
+ return error(`{name}.{tostring(key)} is server-only`, 2)
292
+ end
293
+ end,
294
+ __newindex = function()
295
+ return error(`{name} is a profile store; it has no fields to assign`, 2)
296
+ end,
297
+ }
298
+ meta[RECORD] = declaration
299
+ return meta
300
+ end
301
+ --[[
302
+ *
303
+ * @internal The emitted `profileStore` (rules 1 to 4, 129, 131, 134). Shared-safe: it touches no service and starts
304
+ * nothing, and every check runs before anything registers.
305
+
306
+ ]]
307
+ local function declareProfileStore(name, template, migrations, persist, replica, serverOnlyPaths)
308
+ checkStoreName(name)
309
+ if dataRegistrySealed() then
310
+ error(`{name} was declared after the data registry sealed; declare stores in modules that boot loads`, 3)
311
+ end
312
+ if storeNameTaken(name) then
313
+ error(`{name} is already declared`, 3)
314
+ end
315
+ if #profiles >= MAX_PROFILE_STORES then
316
+ error(`{name}: more than {MAX_PROFILE_STORES} profile stores`, 3)
317
+ end
318
+ if dataDeclarationCount() >= MAX_DATA_DECLARATIONS then
319
+ error(`{name}: more than {MAX_DATA_DECLARATIONS} data declarations`, 3)
320
+ end
321
+ assertDescriptor(persist)
322
+ assertDescriptor(replica)
323
+ if replica.kind ~= "object" then
324
+ error(`{name}: a profile store's value is an object with declared fields`, 3)
325
+ end
326
+ local found = {}
327
+ local unwrapped = unwrap(template, {}, found)
328
+ checkMarks(name, found, serverOnlyPaths)
329
+ local _array = {}
330
+ local _length = #_array
331
+ local _array_1 = (migrations or {})
332
+ table.move(_array_1, 1, #_array_1, _length + 1, _array)
333
+ local steps = _array
334
+ local schema = sealSchema({
335
+ name = name,
336
+ version = #steps,
337
+ defaults = unwrapped,
338
+ node = persist,
339
+ migrations = steps,
340
+ })
341
+ local clientTemplate = admitReplicated(compilePlan(replica), name, schema.defaults)
342
+ local views = declareViews(name, replica)
343
+ local declaration = {
344
+ name = name,
345
+ persist = persist,
346
+ replica = replica,
347
+ schema = schema,
348
+ clientTemplate = clientTemplate,
349
+ live = views.live,
350
+ liveDeclaration = declarationOf(views.live),
351
+ fields = views.fields,
352
+ paths = fieldPaths(replica),
353
+ }
354
+ registerProfileName(name)
355
+ table.insert(profiles, declaration)
356
+ return table.freeze(setmetatable({}, storeMeta(declaration)))
357
+ end
358
+ --* @internal Rule 131: the replica's paths, read from the store's own metatable, never a registry.
359
+ local function replicatedPathsOf(store)
360
+ local _store = store
361
+ local meta = if type(_store) == "table" then (getmetatable(store)) else nil
362
+ local declaration = if type(meta) == "table" then (meta[RECORD]) else nil
363
+ if declaration == nil then
364
+ error("replicatedPaths takes a profile store", 2)
365
+ end
366
+ return declaration.paths
367
+ end
368
+ --* @internal This realm's profile stores, in registration order; DataRuntime binds each when it constructs (104).
369
+ local function profileDeclarations()
370
+ return profiles
371
+ end
372
+ --* @internal The server entry installs its side once per realm, when its module loads (rule 115).
373
+ local function installServerSide(side)
374
+ serverSide = side
375
+ end
376
+ return {
377
+ markServerOnly = markServerOnly,
378
+ clientRoot = clientRoot,
379
+ clientLoaded = clientLoaded,
380
+ declareProfileStore = declareProfileStore,
381
+ replicatedPathsOf = replicatedPathsOf,
382
+ profileDeclarations = profileDeclarations,
383
+ installServerSide = installServerSide,
384
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,318 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _limits = TS.import(script, script.Parent, "limits")
4
+ local INBOX_MAX_MESSAGES = _limits.INBOX_MAX_MESSAGES
5
+ local MAX_PURCHASE_IDS = _limits.MAX_PURCHASE_IDS
6
+ local OPERATION_ID_MAX_BYTES = _limits.OPERATION_ID_MAX_BYTES
7
+ local OPERATION_MARKERS_PER_KEY = _limits.OPERATION_MARKERS_PER_KEY
8
+ local PURCHASE_ID_MAX_BYTES = _limits.PURCHASE_ID_MAX_BYTES
9
+ local QUARANTINE_MAX_MESSAGES = _limits.QUARANTINE_MAX_MESSAGES
10
+ local SEEN_MAX_MESSAGES = _limits.SEEN_MAX_MESSAGES
11
+ --* @internal
12
+ --* @internal The last owned operation that committed (rule 28).
13
+ --* @internal
14
+ --* @internal
15
+ --* @internal
16
+ --* @internal One JSON value per key (rule 38). `payload` is the stored record exactly; bookkeeping is never inside it.
17
+ --* @internal
18
+ local UNREADABLE = {}
19
+ local function fail(reason)
20
+ error({ UNREADABLE, reason })
21
+ end
22
+ local function counter(value, name)
23
+ local _value = value
24
+ local _condition = not (type(_value) == "number")
25
+ if not _condition then
26
+ _condition = value < 0 or value % 1 ~= 0
27
+ end
28
+ if _condition then
29
+ fail(`bad {name}`)
30
+ end
31
+ return value
32
+ end
33
+ local function time(value, name)
34
+ local _value = value
35
+ local _condition = not (type(_value) == "number")
36
+ if not _condition then
37
+ _condition = value ~= value or value < 0
38
+ end
39
+ if _condition then
40
+ fail(`bad {name}`)
41
+ end
42
+ return value
43
+ end
44
+ local function text(value, name, max)
45
+ if max == nil then
46
+ max = math.huge
47
+ end
48
+ local _value = value
49
+ local _condition = not (type(_value) == "string")
50
+ if not _condition then
51
+ _condition = #value == 0 or #value > max
52
+ end
53
+ if _condition then
54
+ fail(`bad {name}`)
55
+ end
56
+ return value
57
+ end
58
+ --* Counts entries before reading any, stopping at the first one past `cap`.
59
+ local function list(value, name, cap)
60
+ if value == nil then
61
+ return {}
62
+ end
63
+ local _value = value
64
+ if not (type(_value) == "table") then
65
+ fail(`bad {name}`)
66
+ end
67
+ local count = 0
68
+ for key in pairs(value) do
69
+ count += 1
70
+ if count > cap then
71
+ fail(`{name} over {cap}`)
72
+ end
73
+ if not (type(key) == "number") or key % 1 ~= 0 or key < 1 then
74
+ fail(`bad {name}`)
75
+ end
76
+ end
77
+ local items = value
78
+ if #items ~= count then
79
+ fail(`bad {name}`)
80
+ end
81
+ return items
82
+ end
83
+ local function ids(value, name, cap, max)
84
+ local _exp = list(value, name, cap)
85
+ -- ▼ ReadonlyArray.map ▼
86
+ local _newValue = table.create(#_exp)
87
+ local _callback = function(id)
88
+ return text(id, `{name} id`, max)
89
+ end
90
+ for _k, _v in _exp do
91
+ _newValue[_k] = _callback(_v, _k - 1, _exp)
92
+ end
93
+ -- ▲ ReadonlyArray.map ▲
94
+ return _newValue
95
+ end
96
+ local function item(raw, name)
97
+ local _raw = raw
98
+ if not (type(_raw) == "table") then
99
+ fail(`bad {name} item`)
100
+ end
101
+ local t = raw
102
+ return {
103
+ id = text(t.id, `{name} id`, OPERATION_ID_MAX_BYTES),
104
+ name = text(t.name, `{name} name`),
105
+ version = counter(t.version, `{name} version`),
106
+ payload = t.payload,
107
+ generation = counter(t.generation, `{name} generation`),
108
+ attempts = counter(t.attempts, `{name} attempts`),
109
+ }
110
+ end
111
+ local function readEnvelope(raw, kind)
112
+ local _raw = raw
113
+ if not (type(_raw) == "table") then
114
+ fail("not a table")
115
+ end
116
+ local t = raw
117
+ if t.format ~= 1 then
118
+ fail("bad format")
119
+ end
120
+ if t.kind ~= kind then
121
+ fail("bad kind")
122
+ end
123
+ local payload = t.payload
124
+ if payload ~= nil and not (type(payload) == "table") then
125
+ fail("bad payload")
126
+ end
127
+ local lease
128
+ if t.lease ~= nil then
129
+ local _lease = t.lease
130
+ if not (type(_lease) == "table") then
131
+ fail("bad lease")
132
+ end
133
+ local l = t.lease
134
+ local _object = {
135
+ token = text(l.token, "lease token"),
136
+ serverId = text(l.serverId, "lease server"),
137
+ }
138
+ local _left = "jobId"
139
+ local _jobId = l.jobId
140
+ _object[_left] = if type(_jobId) == "string" then l.jobId else fail("bad lease job")
141
+ _object.expiresAt = time(l.expiresAt, "lease expiry")
142
+ lease = _object
143
+ end
144
+ local commit
145
+ if t.commit ~= nil then
146
+ local _commit = t.commit
147
+ if not (type(_commit) == "table") then
148
+ fail("bad commit")
149
+ end
150
+ local c = t.commit
151
+ commit = {
152
+ token = text(c.token, "commit token"),
153
+ revision = counter(c.revision, "commit revision"),
154
+ op = counter(c.op, "commit op"),
155
+ }
156
+ end
157
+ local _object = {
158
+ format = 1,
159
+ kind = kind,
160
+ schemaVersion = counter(t.schemaVersion, "schemaVersion"),
161
+ payload = payload,
162
+ epoch = counter(t.epoch, "epoch"),
163
+ lease = lease,
164
+ commit = commit,
165
+ receipts = ids(t.receipts, "receipts", MAX_PURCHASE_IDS, PURCHASE_ID_MAX_BYTES),
166
+ }
167
+ local _left = "inbox"
168
+ local _exp = list(t.inbox, "inbox", INBOX_MAX_MESSAGES)
169
+ -- ▼ ReadonlyArray.map ▼
170
+ local _newValue = table.create(#_exp)
171
+ local _callback = function(raw)
172
+ return item(raw, "inbox")
173
+ end
174
+ for _k, _v in _exp do
175
+ _newValue[_k] = _callback(_v, _k - 1, _exp)
176
+ end
177
+ -- ▲ ReadonlyArray.map ▲
178
+ _object[_left] = _newValue
179
+ local _left_1 = "quarantine"
180
+ local _exp_1 = list(t.quarantine, "quarantine", QUARANTINE_MAX_MESSAGES)
181
+ -- ▼ ReadonlyArray.map ▼
182
+ local _newValue_1 = table.create(#_exp_1)
183
+ local _callback_1 = function(raw)
184
+ local reason = raw.reason
185
+ if reason ~= "payload" and reason ~= "attempts" then
186
+ fail("bad quarantine reason")
187
+ end
188
+ local _object_1 = table.clone(item(raw, "quarantine"))
189
+ setmetatable(_object_1, nil)
190
+ _object_1.reason = reason
191
+ return _object_1
192
+ end
193
+ for _k, _v in _exp_1 do
194
+ _newValue_1[_k] = _callback_1(_v, _k - 1, _exp_1)
195
+ end
196
+ -- ▲ ReadonlyArray.map ▲
197
+ _object[_left_1] = _newValue_1
198
+ _object.seen = ids(t.seen, "seen", SEEN_MAX_MESSAGES, OPERATION_ID_MAX_BYTES)
199
+ local _left_2 = "seenFloor"
200
+ local _condition = t.seenFloor
201
+ if _condition == nil then
202
+ _condition = 0
203
+ end
204
+ _object[_left_2] = time(_condition, "seenFloor")
205
+ _object.operations = ids(t.operations, "operations", OPERATION_MARKERS_PER_KEY, OPERATION_ID_MAX_BYTES)
206
+ local _left_3 = "operationFloor"
207
+ local _condition_1 = t.operationFloor
208
+ if _condition_1 == nil then
209
+ _condition_1 = 0
210
+ end
211
+ _object[_left_3] = time(_condition_1, "operationFloor")
212
+ return _object
213
+ end
214
+ --* @internal Validates before anything is interpreted; a malformed envelope is never repaired (rule 40).
215
+ local function parseEnvelope(raw, kind)
216
+ if raw == nil then
217
+ return {
218
+ kind = "absent",
219
+ }
220
+ end
221
+ local ok, result = pcall(readEnvelope, raw, kind)
222
+ if ok then
223
+ return {
224
+ kind = "ok",
225
+ envelope = result,
226
+ }
227
+ end
228
+ local _binding = result
229
+ local marker = _binding[1]
230
+ local reason = _binding[2]
231
+ if marker == UNREADABLE then
232
+ return {
233
+ kind = "unreadable",
234
+ reason = reason,
235
+ }
236
+ end
237
+ error(result)
238
+ end
239
+ --* @internal An absent key: unopened, epoch 0, empty collections.
240
+ local function emptyEnvelope(kind)
241
+ return {
242
+ format = 1,
243
+ kind = kind,
244
+ schemaVersion = 0,
245
+ epoch = 0,
246
+ receipts = {},
247
+ inbox = {},
248
+ quarantine = {},
249
+ seen = {},
250
+ seenFloor = 0,
251
+ operations = {},
252
+ operationFloor = 0,
253
+ }
254
+ end
255
+ local SHORT_ESCAPES = {
256
+ ['"'] = true,
257
+ ["\\"] = true,
258
+ ["\b"] = true,
259
+ ["\f"] = true,
260
+ ["\n"] = true,
261
+ ["\r"] = true,
262
+ ["\t"] = true,
263
+ }
264
+ local function stringBytes(value)
265
+ local bytes = #value + 2
266
+ for special in string.gmatch(value, '[%c"\\]') do
267
+ bytes += if SHORT_ESCAPES[special] ~= nil then 1 else 5
268
+ end
269
+ return bytes
270
+ end
271
+ --* @internal Encoded JSON bytes, an upper bound on what Roblox counts (rule 39).
272
+ local function jsonBytes(value)
273
+ local _value = value
274
+ if type(_value) == "string" then
275
+ return stringBytes(value)
276
+ end
277
+ local _value_1 = value
278
+ if type(_value_1) == "number" then
279
+ local _result
280
+ if value % 1 == 0 then
281
+ _result = tostring(value)
282
+ else
283
+ local _value_2 = value
284
+ _result = string.format("%.17g", _value_2)
285
+ end
286
+ return #_result
287
+ end
288
+ local _value_2 = value
289
+ if type(_value_2) == "boolean" then
290
+ return if value then 4 else 5
291
+ end
292
+ local _value_3 = value
293
+ if not (type(_value_3) == "table") then
294
+ return 4
295
+ end
296
+ local t = value
297
+ local size = #t
298
+ local entries = 0
299
+ local bytes = 2
300
+ for key, entry in pairs(t) do
301
+ entries += 1
302
+ bytes += jsonBytes(entry)
303
+ if size == 0 or not (type(key) == "number") then
304
+ bytes += stringBytes(tostring(key)) + 1
305
+ end
306
+ end
307
+ return bytes + math.max(entries - 1, 0)
308
+ end
309
+ --* @internal
310
+ local function envelopeBytes(envelope)
311
+ return jsonBytes(envelope)
312
+ end
313
+ return {
314
+ parseEnvelope = parseEnvelope,
315
+ emptyEnvelope = emptyEnvelope,
316
+ jsonBytes = jsonBytes,
317
+ envelopeBytes = envelopeBytes,
318
+ }
@@ -0,0 +1 @@
1
+ export {};