@trioforge/crucible 0.1.0 → 0.2.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 (134) hide show
  1. package/out/client/index.d.ts +7 -0
  2. package/out/client/init.luau +36 -1
  3. package/out/configs.d.ts +37 -0
  4. package/out/configs.luau +46 -0
  5. package/out/data.d.ts +224 -0
  6. package/out/data.luau +97 -0
  7. package/out/emit/configs.d.ts +4 -0
  8. package/out/emit/configs.luau +22 -0
  9. package/out/emit/data.d.ts +6 -0
  10. package/out/emit/data.luau +19 -0
  11. package/out/emit/index.d.ts +5 -1
  12. package/out/emit/init.luau +23 -7
  13. package/out/emit/network.d.ts +6 -0
  14. package/out/emit/network.luau +36 -0
  15. package/out/emit/replicated.d.ts +4 -0
  16. package/out/emit/replicated.luau +13 -0
  17. package/out/index.d.ts +8 -0
  18. package/out/init.luau +23 -0
  19. package/out/internal/binding.luau +5 -0
  20. package/out/internal/boot.luau +107 -8
  21. package/out/internal/configs/limits.d.ts +10 -0
  22. package/out/internal/configs/limits.luau +25 -0
  23. package/out/internal/configs/port.d.ts +29 -0
  24. package/out/internal/configs/port.luau +99 -0
  25. package/out/internal/configs/runtime.d.ts +21 -0
  26. package/out/internal/configs/runtime.luau +981 -0
  27. package/out/internal/data/admission.d.ts +1 -0
  28. package/out/internal/data/admission.luau +673 -0
  29. package/out/internal/data/declarations.d.ts +1 -0
  30. package/out/internal/data/declarations.luau +384 -0
  31. package/out/internal/data/envelope.d.ts +1 -0
  32. package/out/internal/data/envelope.luau +318 -0
  33. package/out/internal/data/handles.d.ts +1 -0
  34. package/out/internal/data/handles.luau +255 -0
  35. package/out/internal/data/inbox-transforms.d.ts +1 -0
  36. package/out/internal/data/inbox-transforms.luau +458 -0
  37. package/out/internal/data/inbox.d.ts +1 -0
  38. package/out/internal/data/inbox.luau +437 -0
  39. package/out/internal/data/lease.d.ts +1 -0
  40. package/out/internal/data/lease.luau +304 -0
  41. package/out/internal/data/limits.d.ts +1 -0
  42. package/out/internal/data/limits.luau +234 -0
  43. package/out/internal/data/messages.d.ts +1 -0
  44. package/out/internal/data/messages.luau +69 -0
  45. package/out/internal/data/operation-id.d.ts +1 -0
  46. package/out/internal/data/operation-id.luau +61 -0
  47. package/out/internal/data/paths.d.ts +2 -0
  48. package/out/internal/data/paths.luau +323 -0
  49. package/out/internal/data/port.d.ts +1 -0
  50. package/out/internal/data/port.luau +290 -0
  51. package/out/internal/data/profiles.d.ts +1 -0
  52. package/out/internal/data/profiles.luau +862 -0
  53. package/out/internal/data/receipts.d.ts +1 -0
  54. package/out/internal/data/receipts.luau +190 -0
  55. package/out/internal/data/record-bindings.d.ts +1 -0
  56. package/out/internal/data/record-bindings.luau +92 -0
  57. package/out/internal/data/record-declarations.d.ts +1 -0
  58. package/out/internal/data/record-declarations.luau +140 -0
  59. package/out/internal/data/records.d.ts +1 -0
  60. package/out/internal/data/records.luau +400 -0
  61. package/out/internal/data/runtime.d.ts +1 -0
  62. package/out/internal/data/runtime.luau +918 -0
  63. package/out/internal/data/scheduler.d.ts +1 -0
  64. package/out/internal/data/scheduler.luau +507 -0
  65. package/out/internal/data/session.d.ts +1 -0
  66. package/out/internal/data/session.luau +234 -0
  67. package/out/internal/data/shutdown.d.ts +1 -0
  68. package/out/internal/data/shutdown.luau +91 -0
  69. package/out/internal/data/waiters.d.ts +1 -0
  70. package/out/internal/data/waiters.luau +65 -0
  71. package/out/internal/lifecycle.luau +45 -0
  72. package/out/internal/network/catalog.d.ts +26 -0
  73. package/out/internal/network/catalog.luau +182 -0
  74. package/out/internal/network/claims.d.ts +1 -0
  75. package/out/internal/network/claims.luau +154 -0
  76. package/out/internal/network/client-session.d.ts +1 -0
  77. package/out/internal/network/client-session.luau +403 -0
  78. package/out/internal/network/codec.d.ts +63 -0
  79. package/out/internal/network/codec.luau +1183 -0
  80. package/out/internal/network/envelope.d.ts +1 -0
  81. package/out/internal/network/envelope.luau +247 -0
  82. package/out/internal/network/limits.d.ts +26 -0
  83. package/out/internal/network/limits.luau +55 -0
  84. package/out/internal/network/port.d.ts +1 -0
  85. package/out/internal/network/port.luau +19 -0
  86. package/out/internal/network/roblox-network-port.d.ts +1 -0
  87. package/out/internal/network/roblox-network-port.luau +105 -0
  88. package/out/internal/network/schema.d.ts +72 -0
  89. package/out/internal/network/schema.luau +386 -0
  90. package/out/internal/network/server-session.d.ts +1 -0
  91. package/out/internal/network/server-session.luau +393 -0
  92. package/out/internal/network/session.d.ts +1 -0
  93. package/out/internal/network/session.luau +266 -0
  94. package/out/internal/reactive/context.d.ts +1 -0
  95. package/out/internal/reactive/context.luau +149 -0
  96. package/out/internal/reactive/flush.d.ts +1 -0
  97. package/out/internal/reactive/flush.luau +355 -0
  98. package/out/internal/reactive/graph.d.ts +1 -0
  99. package/out/internal/reactive/graph.luau +626 -0
  100. package/out/internal/reactive/limits.d.ts +1 -0
  101. package/out/internal/reactive/limits.luau +54 -0
  102. package/out/internal/reactive/ownership.d.ts +1 -0
  103. package/out/internal/reactive/ownership.luau +626 -0
  104. package/out/internal/registry.luau +10 -1
  105. package/out/internal/sync/client-sync.d.ts +1 -0
  106. package/out/internal/sync/client-sync.luau +157 -0
  107. package/out/internal/sync/declarations.d.ts +1 -0
  108. package/out/internal/sync/declarations.luau +276 -0
  109. package/out/internal/sync/frame.d.ts +1 -0
  110. package/out/internal/sync/frame.luau +91 -0
  111. package/out/internal/sync/server-sync.d.ts +1 -0
  112. package/out/internal/sync/server-sync.luau +502 -0
  113. package/out/internal/sync/values.d.ts +1 -0
  114. package/out/internal/sync/values.luau +930 -0
  115. package/out/network.d.ts +66 -0
  116. package/out/network.luau +24 -0
  117. package/out/reactive.d.ts +76 -0
  118. package/out/reactive.luau +136 -0
  119. package/out/replicated.d.ts +38 -0
  120. package/out/replicated.luau +45 -0
  121. package/out/server/data.d.ts +54 -0
  122. package/out/server/data.luau +44 -0
  123. package/out/server/index.d.ts +16 -0
  124. package/out/server/init.luau +49 -3
  125. package/out/server/publish.d.ts +20 -0
  126. package/out/server/publish.luau +24 -0
  127. package/out/vide/index.d.ts +7 -0
  128. package/out/vide/init.luau +50 -0
  129. package/package.json +13 -2
  130. package/transformer/data.cjs +72 -0
  131. package/transformer/declarations.cjs +153 -0
  132. package/transformer/index.js +37 -11
  133. package/transformer/network.cjs +961 -0
  134. package/vide/package.json +4 -0
@@ -1,3 +1,10 @@
1
1
  import type { BootOptions } from "../index";
2
+ import type { AskResult, AskServer, PayloadArgs, ToServer } from "../network";
2
3
  export declare const BOOTED_ATTRIBUTE = "CrucibleBooted";
3
4
  export declare function boot(options: BootOptions): void;
5
+ export declare namespace Network {
6
+ /** True when accepted locally: fired now, or queued until this client is ready. Never delivery. */
7
+ function send<P>(message: ToServer<P>, ...payload: PayloadArgs<NoInfer<P>>): boolean;
8
+ /** Yields until the reply, the declared timeout or the session ending. */
9
+ function ask<Request, Response>(message: AskServer<Request, Response>, ...request: PayloadArgs<NoInfer<Request>>): AskResult<Response>;
10
+ }
@@ -1,20 +1,55 @@
1
1
  -- Compiled with roblox-ts v3.0.0
2
2
  local TS = _G[script]
3
3
  local bootSide = TS.import(script, script.Parent, "internal", "boot").boot
4
+ local ClientSession = TS.import(script, script.Parent, "internal", "network", "client-session").ClientSession
5
+ local networkPort = TS.import(script, script.Parent, "internal", "network", "port").networkPort
6
+ local installClientSync = TS.import(script, script.Parent, "internal", "sync", "client-sync").installClientSync
7
+ local SyncFaultMessage = TS.import(script, script.Parent, "internal", "sync", "frame").SyncFaultMessage
4
8
  if not game:GetService("RunService"):IsClient() then
5
9
  error("@trioforge/crucible/client loaded on the server")
6
10
  end
7
11
  local BOOTED_ATTRIBUTE = "CrucibleBooted"
12
+ local session
13
+ local function active()
14
+ return session or error("Crucible networking is not ready; send from a constructor, a hook, or later", 0)
15
+ end
8
16
  local function boot(options)
17
+ local network = ClientSession.new(networkPort())
18
+ session = network
19
+ installClientSync(function(revision)
20
+ return active():send(SyncFaultMessage, revision)
21
+ end)
9
22
  bootSide({
10
23
  name = "client",
11
24
  renders = true,
12
25
  booted = function()
13
- return game:GetService("Players").LocalPlayer:SetAttribute(BOOTED_ATTRIBUTE, true)
26
+ local player = game:GetService("Players").LocalPlayer
27
+ player:SetAttribute(BOOTED_ATTRIBUTE, true)
28
+ return function()
29
+ return player:SetAttribute(BOOTED_ATTRIBUTE, nil)
30
+ end
14
31
  end,
32
+ network = network,
15
33
  }, options)
16
34
  end
35
+ local Network = {}
36
+ do
37
+ local _container = Network
38
+ --* True when accepted locally: fired now, or queued until this client is ready. Never delivery.
39
+ local function send(message, ...)
40
+ local payload = { ... }
41
+ return active():send(message, payload[1])
42
+ end
43
+ _container.send = send
44
+ --* Yields until the reply, the declared timeout or the session ending.
45
+ local function ask(message, ...)
46
+ local request = { ... }
47
+ return active():ask(message, request[1])
48
+ end
49
+ _container.ask = ask
50
+ end
17
51
  return {
18
52
  boot = boot,
19
53
  BOOTED_ATTRIBUTE = BOOTED_ATTRIBUTE,
54
+ Network = Network,
20
55
  }
@@ -0,0 +1,37 @@
1
+ declare const configKind: unique symbol;
2
+ /** An experience-wide config. Calling it reads the global snapshot's value; it never yields or throws. */
3
+ export interface Config<T> {
4
+ (): T;
5
+ readonly [configKind]: {
6
+ readonly scope: "global";
7
+ readonly value: (value: T) => T;
8
+ };
9
+ }
10
+ /** A targeted config. Calling it reads that player's value; it never yields or throws. */
11
+ export interface PlayerConfig<T> {
12
+ (player: Player): T;
13
+ readonly [configKind]: {
14
+ readonly scope: "player";
15
+ readonly value: (value: T) => T;
16
+ };
17
+ }
18
+ /**
19
+ * Declares an experience config by its dashboard key, read from the global snapshot. It must initialize an exported
20
+ * top-level const in a server module, with `T` explicit. The fallback is the value before the first fetch and for a
21
+ * missing key.
22
+ */
23
+ export declare function config<T = never>(_name: string, _fallback: NoInfer<T>): Config<T>;
24
+ /** Declares a targeted config by its dashboard key, read from each player's snapshot. Same form as `config`. */
25
+ export declare function playerConfig<T = never>(_name: string, _fallback: NoInfer<T>): PlayerConfig<T>;
26
+ /**
27
+ * With no argument, true once the global snapshot has been applied. With a player, true once that player's snapshot
28
+ * has been applied, and false before and after they leave.
29
+ */
30
+ export declare function configReady(player?: Player): boolean;
31
+ /**
32
+ * Refreshes every snapshot with newer values, fetches every snapshot that has not loaded, and applies them all in
33
+ * one pass. Yields, at most 10 seconds. True when every snapshot is current; false keeps the last good values.
34
+ * Concurrent calls share one refresh. Crucible never calls it.
35
+ */
36
+ export declare function refreshConfigs(): boolean;
37
+ export {};
@@ -0,0 +1,46 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _runtime = TS.import(script, script.Parent, "internal", "configs", "runtime")
4
+ local ready = _runtime.ready
5
+ local refresh = _runtime.refresh
6
+ --* An experience-wide config. Calling it reads the global snapshot's value; it never yields or throws.
7
+ --* A targeted config. Calling it reads that player's value; it never yields or throws.
8
+ --[[
9
+ *
10
+ * Declares an experience config by its dashboard key, read from the global snapshot. It must initialize an exported
11
+ * top-level const in a server module, with `T` explicit. The fallback is the value before the first fetch and for a
12
+ * missing key.
13
+
14
+ ]]
15
+ local function config(_name, _fallback)
16
+ error("Crucible configs need the @trioforge/crucible/transformer plugin")
17
+ end
18
+ --* Declares a targeted config by its dashboard key, read from each player's snapshot. Same form as `config`.
19
+ local function playerConfig(_name, _fallback)
20
+ error("Crucible configs need the @trioforge/crucible/transformer plugin")
21
+ end
22
+ --[[
23
+ *
24
+ * With no argument, true once the global snapshot has been applied. With a player, true once that player's snapshot
25
+ * has been applied, and false before and after they leave.
26
+
27
+ ]]
28
+ local function configReady(player)
29
+ return ready(player)
30
+ end
31
+ --[[
32
+ *
33
+ * Refreshes every snapshot with newer values, fetches every snapshot that has not loaded, and applies them all in
34
+ * one pass. Yields, at most 10 seconds. True when every snapshot is current; false keeps the last good values.
35
+ * Concurrent calls share one refresh. Crucible never calls it.
36
+
37
+ ]]
38
+ local function refreshConfigs()
39
+ return refresh()
40
+ end
41
+ return {
42
+ config = config,
43
+ playerConfig = playerConfig,
44
+ configReady = configReady,
45
+ refreshConfigs = refreshConfigs,
46
+ }
package/out/data.d.ts ADDED
@@ -0,0 +1,224 @@
1
+ import type { Readable } from "./reactive";
2
+ declare const operationKind: unique symbol;
3
+ declare const storeKind: unique symbol;
4
+ declare const messageKind: unique symbol;
5
+ declare const claimKind: unique symbol;
6
+ declare const serverOnlyKind: unique symbol;
7
+ /** A value a save can hold: JSON, with absent keys for values that admit `undefined`. */
8
+ export type Json = boolean | number | string | readonly Json[] | JsonRecord;
9
+ /** A saved record: string keys to JSON. */
10
+ export interface JsonRecord {
11
+ readonly [key: string]: Json | undefined;
12
+ }
13
+ /**
14
+ * One schema step. It gets a private copy of the stored record, unknown keys included, as plain JSON, and returns a
15
+ * patch: an object merges into the stored object, any other value replaces the stored one, and an omitted key is kept.
16
+ * A step cannot delete a key. Steps are append-only and must be pure.
17
+ */
18
+ export type Migration = (old: JsonRecord) => JsonRecord;
19
+ /**
20
+ * The identity of one logical operation, kept across its retries. Only `operationId()` makes one; a plain string does
21
+ * not type-check. It carries its creation time, which bounds how long it deduplicates.
22
+ */
23
+ export type OperationId = string & {
24
+ readonly [operationKind]: true;
25
+ };
26
+ /** Why a store call did not complete. */
27
+ export type StoreFailure = "unavailable" | "timeout" | "uncertain" | "overloaded" | "full" | "expired" | "reused" | "closing";
28
+ /** A store call's result: narrow `ok` before reading `value`. */
29
+ export type StoreResult<T> = {
30
+ readonly ok: true;
31
+ readonly value: T;
32
+ } | {
33
+ readonly ok: false;
34
+ readonly reason: StoreFailure;
35
+ };
36
+ /**
37
+ * A template field the owner's client never receives. The brand is phantom; `serverOnly(value)` also marks the value at
38
+ * runtime, and the declaration checks that the two agree.
39
+ */
40
+ export interface ServerOnly<V> {
41
+ readonly [serverOnlyKind]: V;
42
+ }
43
+ type Scalar = string | number | boolean;
44
+ /** What the server reads and writes: the template's type with every `ServerOnly` unwrapped, deeply readonly. */
45
+ export type ProfileData<T> = T extends ServerOnly<infer V> ? ProfileData<V> : T extends Scalar | undefined ? T : T extends ReadonlyMap<infer K, infer V> ? ReadonlyMap<K, ProfileData<V>> : {
46
+ readonly [K in keyof T]: ProfileData<T[K]>;
47
+ };
48
+ type IsUnion<V, C = V> = V extends unknown ? ([C] extends [V] ? false : true) : never;
49
+ /** A `ServerOnly` field, or a plain object field whose every field is server-only in turn. */
50
+ type ServerOnlyField<V> = [V] extends [ServerOnly<unknown>] ? true : [V] extends [Scalar | undefined] ? false : [V] extends [ReadonlyMap<unknown, unknown> | readonly unknown[]] ? false : true extends IsUnion<V> ? false : string extends keyof V ? false : [keyof V] extends [never] ? false : false extends {
51
+ [K in keyof V]-?: ServerOnlyField<NonNullable<V[K]>>;
52
+ }[keyof V] ? false : true;
53
+ /**
54
+ * What the owner's client reads: every `ServerOnly` field removed, and every plain object left with no field, at any
55
+ * depth. Deeply readonly.
56
+ */
57
+ export type ClientData<T> = T extends Scalar | undefined ? T : T extends ReadonlyMap<infer K, infer V> ? ReadonlyMap<K, ClientData<V>> : T extends readonly unknown[] ? {
58
+ readonly [K in keyof T]: ClientData<T[K]>;
59
+ } : {
60
+ readonly [K in keyof T as ServerOnlyField<NonNullable<T[K]>> extends true ? never : K]: ClientData<T[K]>;
61
+ };
62
+ /** A store's server data type, for helper signatures: `export type PlayerData = Profile<typeof PlayerData>`. */
63
+ export type Profile<S> = S extends ProfileStore<infer T> ? ProfileData<T> : never;
64
+ /** A store's client data type. */
65
+ export type ClientProfile<S> = S extends ProfileStore<infer T> ? ClientData<T> : never;
66
+ /**
67
+ * A server handle's own members. `h()` reads, tracked and frozen; while the profile is not writable it returns the
68
+ * template's value at the path. `set` and `update` are one write each through the store's gate and return its result.
69
+ */
70
+ export interface WriteHandle<V, A> extends Readable<V | A> {
71
+ set(value: V): boolean;
72
+ update(change: (current: V) => V): boolean;
73
+ }
74
+ /** An array handle's writes. Indices are TypeScript's, from 0. */
75
+ export interface ArrayWrites<E> {
76
+ /** Inserts at `index`, from 0 to the length, or appends without one. */
77
+ insert(value: E, index?: number): boolean;
78
+ /** Removes the element at `index`, from 0 to length - 1. */
79
+ remove(index: number): boolean;
80
+ }
81
+ type DataChildren<V, A> = [V] extends [Scalar] ? unknown : [V] extends [readonly (infer E)[]] ? ArrayWrites<E> & {
82
+ readonly [index: number]: DataHandle<E, undefined>;
83
+ } : true extends IsUnion<V> ? unknown : [V] extends [ReadonlyMap<string, infer E>] ? {
84
+ readonly [key: string]: DataHandle<E | undefined, A>;
85
+ } : string extends keyof V ? {
86
+ readonly [key: string]: DataHandle<V[string & keyof V] | undefined, A>;
87
+ } : {
88
+ readonly [K in keyof V]-?: DataHandle<V[K], A>;
89
+ };
90
+ /** A server handle at a position of type `V`: reads `V | A`, writes `V`, and reaches its children by key. */
91
+ export type DataHandle<V, A = never> = WriteHandle<V, A> & DataChildren<NonNullable<V>, A | Extract<V, undefined>>;
92
+ type ReadChildren<V, A> = [V] extends [Scalar] ? unknown : [V] extends [readonly (infer E)[]] ? {
93
+ readonly [index: number]: ReadHandle<E, undefined>;
94
+ } : true extends IsUnion<V> ? unknown : [V] extends [ReadonlyMap<string, infer E>] ? {
95
+ readonly [key: string]: ReadHandle<E | undefined, A>;
96
+ } : string extends keyof V ? {
97
+ readonly [key: string]: ReadHandle<V[string & keyof V] | undefined, A>;
98
+ } : {
99
+ readonly [K in keyof V]-?: ReadHandle<V[K], A>;
100
+ };
101
+ /** A client handle: the same paths and reads as the server's, and no writes. */
102
+ export type ReadHandle<V, A = never> = Readable<V | A> & ReadChildren<NonNullable<V>, A | Extract<V, undefined>>;
103
+ /** Why a profile loaded. */
104
+ export type LoadReason = "join" | "resume";
105
+ /** Why a profile is releasing. */
106
+ export type ReleaseReason = "leave" | "handoff" | "shutdown";
107
+ /** A developer product purchase, read from Roblox's receipt. */
108
+ export interface Purchase {
109
+ readonly id: string;
110
+ readonly productId: number;
111
+ readonly currencySpent: number;
112
+ }
113
+ /**
114
+ * A grant callback's fourth argument. It stages an effect that runs once, on a fresh thread, after the grant is durably
115
+ * saved and before the purchase is acknowledged; never after a refused grant.
116
+ */
117
+ export type AfterGrant = (effect: () => void) => void;
118
+ /** A field holding it is the claim. Not callable. */
119
+ export interface ProfileClaim {
120
+ readonly [claimKind]: true;
121
+ }
122
+ /** A durable message to one player's profile, declared in a server module. Invariant in `P`. */
123
+ export interface ProfileMessage<P> {
124
+ readonly [messageKind]: {
125
+ readonly payload: (value: P) => P;
126
+ };
127
+ }
128
+ /** One quarantined message, for recovery tooling. The payload is the stored JSON, unknown keys included. */
129
+ export interface QuarantinedMessage {
130
+ readonly id: OperationId;
131
+ readonly name: string;
132
+ readonly reason: "payload" | "attempts";
133
+ readonly attempts: number;
134
+ /** The id's creation time, in wall-clock seconds. */
135
+ readonly sentAt: number;
136
+ readonly payload: Json;
137
+ }
138
+ /** Why a save or handoff did not confirm. */
139
+ export type SaveFailure = "unavailable" | "timeout" | "uncertain";
140
+ /** A save or handoff result: narrow `ok` first. */
141
+ export type SaveResult = {
142
+ readonly ok: true;
143
+ } | {
144
+ readonly ok: false;
145
+ readonly reason: SaveFailure;
146
+ };
147
+ /**
148
+ * A profile store: one shared declaration, callable on both sides. The server calls it with a player for that player's
149
+ * writable handle; the client calls it with nothing for the local player's read-only handle.
150
+ */
151
+ export interface ProfileStore<T> {
152
+ readonly [storeKind]: {
153
+ readonly kind: "profiles";
154
+ readonly value: (value: T) => T;
155
+ };
156
+ /** Server: the player's root handle, the same one while they are present. */
157
+ (player: Player): DataHandle<ProfileData<T>>;
158
+ /** Client: the local player's read-only root handle. */
159
+ (): ReadHandle<ClientData<T>>;
160
+ /** Server. Tracked: whether a write to the player's profile would be accepted now. */
161
+ loaded(player: Player): boolean;
162
+ /** Client. Tracked: whether the server has published this player's profile and the client holds all of it. */
163
+ loaded(): boolean;
164
+ /** Server. Tracked: the players whose profiles are writable; it changes only when one starts or stops being. */
165
+ players(): readonly Player[];
166
+ /** Server. Yields until the profile is writable (true), or its load fails, the player leaves or the server closes. */
167
+ await(player: Player): boolean;
168
+ /** Server. Yields until the current revision is durably saved, for a rare critical point. */
169
+ save(player: Player, timeoutSeconds?: number): SaveResult;
170
+ /** Server. Seals and durably releases the profile while the player stays, before a teleport. */
171
+ handoff(player: Player): SaveResult;
172
+ /** Server. After a failed teleport: waits for the handoff's release to settle, then loads again. */
173
+ resume(player: Player): boolean;
174
+ /** Server. Reads any user's saved data, with no lock and no write. */
175
+ inspect(userId: number): StoreResult<ProfileData<T> | undefined>;
176
+ /** Server. Appends a message to a user's inbox, online or not, and waits until it is durable. */
177
+ send<P>(userId: number, message: ProfileMessage<P>, payload: NoInfer<P>, id?: OperationId): StoreResult<undefined>;
178
+ /** Server. A user's quarantined messages, read with no lock. */
179
+ quarantined(userId: number): StoreResult<readonly QuarantinedMessage[]>;
180
+ /** Server. Moves listed quarantined messages back to the inbox ("retry") or drops them for good ("discard"). */
181
+ clearQuarantined(userId: number, ids: readonly OperationId[], action: "retry" | "discard"): StoreResult<number>;
182
+ /** Server claim, a Subsystem field initializer: changes a profile as it loads, before anything reads it. */
183
+ onLoad(transform: (player: Player, data: ProfileData<T>, reason: LoadReason) => ProfileData<T>): ProfileClaim;
184
+ /** Server claim: changes a profile as it releases. A claim that throws is skipped. */
185
+ onRelease(transform: (player: Player, data: ProfileData<T>, reason: ReleaseReason) => ProfileData<T>): ProfileClaim;
186
+ /** Server claim: applies a message to its target's profile, once per message id. */
187
+ receive<P>(message: ProfileMessage<P>, apply: (player: Player, data: ProfileData<T>, payload: P) => ProfileData<T>): ProfileClaim;
188
+ /** Server claim: grants a developer product. The purchase is acknowledged only after the change is saved. */
189
+ grant(productId: number, apply: (player: Player, data: ProfileData<T>, purchase: Purchase, afterGrant: AfterGrant) => ProfileData<T>): ProfileClaim;
190
+ }
191
+ /** A profile store's options. */
192
+ export interface ProfileOptions<T> {
193
+ /**
194
+ * The store's type, by inference, and a new profile's value. A missing required key on an object path from the root
195
+ * takes its value from here. Admitted and frozen once, with every `serverOnly` mark unwrapped.
196
+ */
197
+ readonly template: T;
198
+ /** Append-only. Removing or reordering a step changes what every older record becomes. */
199
+ readonly migrations?: readonly Migration[];
200
+ }
201
+ /** The handle member names. A template key with one of these names, at any depth, does not compile. */
202
+ export type ReservedName = "set" | "update" | "insert" | "remove";
203
+ type ReservedKeys<T> = T extends ServerOnly<infer V> ? ReservedKeys<V> : T extends Scalar | undefined ? never : T extends ReadonlyMap<unknown, infer V> ? ReservedKeys<V> : T extends readonly (infer E)[] ? ReservedKeys<E> : string extends keyof T ? ReservedKeys<T[string & keyof T]> : (keyof T & ReservedName) | {
204
+ [K in keyof T]-?: ReservedKeys<T[K]>;
205
+ }[keyof T];
206
+ type ReservedKeyCheck<T> = [ReservedKeys<T>] extends [never] ? unknown : {
207
+ readonly template: {
208
+ readonly [K in ReservedKeys<T> as `"${K & string}" is a handle method; rename it`]: never;
209
+ };
210
+ };
211
+ /**
212
+ * Declares a profile store by its DataStore name. It must initialize an exported top-level const in a shared module.
213
+ * The type comes from `template`; annotate empty collections and optional fields with `as`.
214
+ */
215
+ export declare function profileStore<T extends object>(_name: string, _options: ProfileOptions<T> & ReservedKeyCheck<T>): ProfileStore<T>;
216
+ /** Marks a template field server-only: the owner's client never receives it. */
217
+ export declare function serverOnly<V>(value: V): ServerOnly<V>;
218
+ /**
219
+ * The exact field paths the owner's client receives, each an array of segments: an object field's key, "[]" for an
220
+ * array element and "*" for a map value. A key that is "[]" or "*", or starts with a backslash, gets one more backslash.
221
+ * Sorted segment by segment, by bytes.
222
+ */
223
+ export declare function replicatedPaths<T>(store: ProfileStore<T>): readonly (readonly string[])[];
224
+ export {};
package/out/data.luau ADDED
@@ -0,0 +1,97 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _declarations = TS.import(script, script.Parent, "internal", "data", "declarations")
4
+ local markServerOnly = _declarations.markServerOnly
5
+ local replicatedPathsOf = _declarations.replicatedPathsOf
6
+ --* A value a save can hold: JSON, with absent keys for values that admit `undefined`.
7
+ --* A saved record: string keys to JSON.
8
+ --[[
9
+ *
10
+ * One schema step. It gets a private copy of the stored record, unknown keys included, as plain JSON, and returns a
11
+ * patch: an object merges into the stored object, any other value replaces the stored one, and an omitted key is kept.
12
+ * A step cannot delete a key. Steps are append-only and must be pure.
13
+
14
+ ]]
15
+ --[[
16
+ *
17
+ * The identity of one logical operation, kept across its retries. Only `operationId()` makes one; a plain string does
18
+ * not type-check. It carries its creation time, which bounds how long it deduplicates.
19
+
20
+ ]]
21
+ --* Why a store call did not complete.
22
+ --* A store call's result: narrow `ok` before reading `value`.
23
+ --[[
24
+ *
25
+ * A template field the owner's client never receives. The brand is phantom; `serverOnly(value)` also marks the value at
26
+ * runtime, and the declaration checks that the two agree.
27
+
28
+ ]]
29
+ --* What the server reads and writes: the template's type with every `ServerOnly` unwrapped, deeply readonly.
30
+ --* A `ServerOnly` field, or a plain object field whose every field is server-only in turn.
31
+ --[[
32
+ *
33
+ * What the owner's client reads: every `ServerOnly` field removed, and every plain object left with no field, at any
34
+ * depth. Deeply readonly.
35
+
36
+ ]]
37
+ --* A store's server data type, for helper signatures: `export type PlayerData = Profile<typeof PlayerData>`.
38
+ --* A store's client data type.
39
+ --[[
40
+ *
41
+ * A server handle's own members. `h()` reads, tracked and frozen; while the profile is not writable it returns the
42
+ * template's value at the path. `set` and `update` are one write each through the store's gate and return its result.
43
+
44
+ ]]
45
+ --* An array handle's writes. Indices are TypeScript's, from 0.
46
+ --* A server handle at a position of type `V`: reads `V | A`, writes `V`, and reaches its children by key.
47
+ --* A client handle: the same paths and reads as the server's, and no writes.
48
+ --* Why a profile loaded.
49
+ --* Why a profile is releasing.
50
+ --* A developer product purchase, read from Roblox's receipt.
51
+ --[[
52
+ *
53
+ * A grant callback's fourth argument. It stages an effect that runs once, on a fresh thread, after the grant is durably
54
+ * saved and before the purchase is acknowledged; never after a refused grant.
55
+
56
+ ]]
57
+ --* A field holding it is the claim. Not callable.
58
+ --* A durable message to one player's profile, declared in a server module. Invariant in `P`.
59
+ --* One quarantined message, for recovery tooling. The payload is the stored JSON, unknown keys included.
60
+ --* Why a save or handoff did not confirm.
61
+ --* A save or handoff result: narrow `ok` first.
62
+ --[[
63
+ *
64
+ * A profile store: one shared declaration, callable on both sides. The server calls it with a player for that player's
65
+ * writable handle; the client calls it with nothing for the local player's read-only handle.
66
+
67
+ ]]
68
+ --* A profile store's options.
69
+ --* The handle member names. A template key with one of these names, at any depth, does not compile.
70
+ --[[
71
+ *
72
+ * Declares a profile store by its DataStore name. It must initialize an exported top-level const in a shared module.
73
+ * The type comes from `template`; annotate empty collections and optional fields with `as`.
74
+
75
+ ]]
76
+ local function profileStore(_name, _options)
77
+ error("Crucible data needs the @trioforge/crucible/transformer plugin")
78
+ end
79
+ --* Marks a template field server-only: the owner's client never receives it.
80
+ local function serverOnly(value)
81
+ return markServerOnly(value)
82
+ end
83
+ --[[
84
+ *
85
+ * The exact field paths the owner's client receives, each an array of segments: an object field's key, "[]" for an
86
+ * array element and "*" for a map value. A key that is "[]" or "*", or starts with a backslash, gets one more backslash.
87
+ * Sorted segment by segment, by bytes.
88
+
89
+ ]]
90
+ local function replicatedPaths(store)
91
+ return replicatedPathsOf(store)
92
+ end
93
+ return {
94
+ profileStore = profileStore,
95
+ serverOnly = serverOnly,
96
+ replicatedPaths = replicatedPaths,
97
+ }
@@ -0,0 +1,4 @@
1
+ import type { Config, PlayerConfig } from "../configs";
2
+ import type { Node } from "../internal/network/schema";
3
+ export declare function config<T>(name: string, fallback: T, value: Node): Config<T>;
4
+ export declare function playerConfig<T>(name: string, fallback: T, value: Node): PlayerConfig<T>;
@@ -0,0 +1,22 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _runtime = TS.import(script, script.Parent.Parent, "internal", "configs", "runtime")
4
+ local declare = _runtime.declare
5
+ local readGlobal = _runtime.readGlobal
6
+ local readPlayer = _runtime.readPlayer
7
+ local function config(name, fallback, value)
8
+ local declaration = declare(name, "global", fallback, value)
9
+ return function()
10
+ return readGlobal(declaration)
11
+ end
12
+ end
13
+ local function playerConfig(name, fallback, value)
14
+ local declaration = declare(name, "player", fallback, value)
15
+ return function(player)
16
+ return readPlayer(declaration, player)
17
+ end
18
+ end
19
+ return {
20
+ config = config,
21
+ playerConfig = playerConfig,
22
+ }
@@ -0,0 +1,6 @@
1
+ import type { ProfileMessage, ProfileOptions, ProfileStore } from "../data";
2
+ import type { Node } from "../internal/network/schema";
3
+ import type { DataStore, StoreOptions } from "../server/data";
4
+ export declare function dataStore<T extends object>(name: string, options: StoreOptions<NoInfer<T>>, value: Node): DataStore<T>;
5
+ export declare function profileStore<T extends object>(name: string, options: ProfileOptions<T>, persist: Node, replica: Node, serverOnlyPaths: readonly (readonly string[])[]): ProfileStore<T>;
6
+ export declare function profileMessage<P>(name: string, payload: Node): ProfileMessage<P>;
@@ -0,0 +1,19 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local declareProfileStore = TS.import(script, script.Parent.Parent, "internal", "data", "declarations").declareProfileStore
4
+ local declareProfileMessage = TS.import(script, script.Parent.Parent, "internal", "data", "messages").declareProfileMessage
5
+ local declareRecords = TS.import(script, script.Parent.Parent, "internal", "data", "record-declarations").declareRecords
6
+ local function dataStore(name, options, value)
7
+ return declareRecords(name, options.defaults, options.migrations, value)
8
+ end
9
+ local function profileStore(name, options, persist, replica, serverOnlyPaths)
10
+ return declareProfileStore(name, options.template, options.migrations, persist, replica, serverOnlyPaths)
11
+ end
12
+ local function profileMessage(name, payload)
13
+ return declareProfileMessage(name, payload)
14
+ end
15
+ return {
16
+ dataStore = dataStore,
17
+ profileStore = profileStore,
18
+ profileMessage = profileMessage,
19
+ }
@@ -1,3 +1,8 @@
1
+ export { n } from "../internal/network/schema";
2
+ export { config, playerConfig } from "./configs";
3
+ export { toServer, toClient, askServer, receive } from "./network";
4
+ export { replicated, replicatedPerPlayer } from "./replicated";
5
+ export { dataStore, profileStore, profileMessage } from "./data";
1
6
  import type { All } from "../all";
2
7
  import type { CrucibleObject } from "../crucible-object";
3
8
  export type EmittedDep = string | {
@@ -38,4 +43,3 @@ export interface EmittedAttribute {
38
43
  readonly required: boolean;
39
44
  }
40
45
  export declare function attribute<T>(object: object, field: string, fallback?: T): T;
41
- export {};
@@ -1,6 +1,23 @@
1
1
  -- Compiled with roblox-ts v3.0.0
2
2
  local TS = _G[script]
3
+ local exports = {}
3
4
  local readClaim = TS.import(script, script.Parent, "internal", "construction-claim").readClaim
5
+ exports.n = TS.import(script, script.Parent, "internal", "network", "schema").n
6
+ local _configs = TS.import(script, script, "configs")
7
+ exports.config = _configs.config
8
+ exports.playerConfig = _configs.playerConfig
9
+ local _network = TS.import(script, script, "network")
10
+ exports.toServer = _network.toServer
11
+ exports.toClient = _network.toClient
12
+ exports.askServer = _network.askServer
13
+ exports.receive = _network.receive
14
+ local _replicated = TS.import(script, script, "replicated")
15
+ exports.replicated = _replicated.replicated
16
+ exports.replicatedPerPlayer = _replicated.replicatedPerPlayer
17
+ local _data = TS.import(script, script, "data")
18
+ exports.dataStore = _data.dataStore
19
+ exports.profileStore = _data.profileStore
20
+ exports.profileMessage = _data.profileMessage
4
21
  local _registry = TS.import(script, script.Parent, "internal", "registry")
5
22
  local define = _registry.define
6
23
  local resolveOne = _registry.resolveOne
@@ -76,10 +93,9 @@ local function attribute(object, field, fallback)
76
93
  local value = _value
77
94
  return if value == nil then fallback else value
78
95
  end
79
- return {
80
- defineClass = defineClass,
81
- resolve = resolve,
82
- resolveAll = resolveAll,
83
- attribute = attribute,
84
- t = t,
85
- }
96
+ exports.defineClass = defineClass
97
+ exports.resolve = resolve
98
+ exports.resolveAll = resolveAll
99
+ exports.attribute = attribute
100
+ exports.t = t
101
+ return exports
@@ -0,0 +1,6 @@
1
+ import type { AskServer, ReceiveCallback, Receiver, ReceiveTarget, ToClient, ToServer } from "../network";
2
+ import type { Node } from "../internal/network/schema";
3
+ export declare function toServer<P>(name: string, payload: Node): ToServer<P>;
4
+ export declare function toClient<P>(name: string, payload: Node): ToClient<P>;
5
+ export declare function askServer<Request, Response>(name: string, request: Node, response: Node, timeout: number | undefined): AskServer<Request, Response>;
6
+ export declare function receive<T extends ReceiveTarget>(owner: object, field: string, target: T, callback: ReceiveCallback<T>): Receiver;
@@ -0,0 +1,36 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local define = TS.import(script, script.Parent.Parent, "internal", "network", "catalog").define
4
+ local claim = TS.import(script, script.Parent.Parent, "internal", "network", "claims").claim
5
+ local function toServer(name, payload)
6
+ return define({
7
+ kind = "to-server",
8
+ name = name,
9
+ input = payload,
10
+ })
11
+ end
12
+ local function toClient(name, payload)
13
+ return define({
14
+ kind = "to-client",
15
+ name = name,
16
+ input = payload,
17
+ })
18
+ end
19
+ local function askServer(name, request, response, timeout)
20
+ return define({
21
+ kind = "ask-server",
22
+ name = name,
23
+ input = request,
24
+ output = response,
25
+ timeout = timeout,
26
+ })
27
+ end
28
+ local function receive(owner, field, target, callback)
29
+ return claim(owner, field, target, callback)
30
+ end
31
+ return {
32
+ toServer = toServer,
33
+ toClient = toClient,
34
+ askServer = askServer,
35
+ receive = receive,
36
+ }
@@ -0,0 +1,4 @@
1
+ import type { Node } from "../internal/network/schema";
2
+ import type { Replicated, ReplicatedPerPlayer } from "../replicated";
3
+ export declare function replicated<T>(name: string, initial: NoInfer<T>, value: Node): Replicated<T>;
4
+ export declare function replicatedPerPlayer<T>(name: string, initial: NoInfer<T>, value: Node): ReplicatedPerPlayer<T>;
@@ -0,0 +1,13 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local declareReplicated = TS.import(script, script.Parent.Parent, "internal", "sync", "declarations").declareReplicated
4
+ local function replicated(name, initial, value)
5
+ return declareReplicated("all", name, initial, value)
6
+ end
7
+ local function replicatedPerPlayer(name, initial, value)
8
+ return declareReplicated("player", name, initial, value)
9
+ end
10
+ return {
11
+ replicated = replicated,
12
+ replicatedPerPlayer = replicatedPerPlayer,
13
+ }