@trioforge/crucible 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/client/package.json +4 -0
- package/emit/package.json +4 -0
- package/out/all.d.ts +7 -0
- package/out/all.luau +2 -0
- package/out/attribute.d.ts +7 -0
- package/out/attribute.luau +7 -0
- package/out/client/index.d.ts +10 -0
- package/out/client/init.luau +55 -0
- package/out/configs.d.ts +37 -0
- package/out/configs.luau +46 -0
- package/out/crucible-behaviour.d.ts +16 -0
- package/out/crucible-behaviour.luau +78 -0
- package/out/crucible-object.d.ts +10 -0
- package/out/crucible-object.luau +69 -0
- package/out/data.d.ts +224 -0
- package/out/data.luau +97 -0
- package/out/emit/configs.d.ts +4 -0
- package/out/emit/configs.luau +22 -0
- package/out/emit/data.d.ts +6 -0
- package/out/emit/data.luau +19 -0
- package/out/emit/index.d.ts +45 -0
- package/out/emit/init.luau +101 -0
- package/out/emit/network.d.ts +6 -0
- package/out/emit/network.luau +36 -0
- package/out/emit/replicated.d.ts +4 -0
- package/out/emit/replicated.luau +13 -0
- package/out/index.d.ts +20 -0
- package/out/init.luau +34 -0
- package/out/inject.d.ts +1 -0
- package/out/inject.luau +7 -0
- package/out/internal/binding.d.ts +1 -0
- package/out/internal/binding.luau +615 -0
- package/out/internal/boot.d.ts +1 -0
- package/out/internal/boot.luau +562 -0
- package/out/internal/configs/limits.d.ts +10 -0
- package/out/internal/configs/limits.luau +25 -0
- package/out/internal/configs/port.d.ts +29 -0
- package/out/internal/configs/port.luau +99 -0
- package/out/internal/configs/runtime.d.ts +21 -0
- package/out/internal/configs/runtime.luau +981 -0
- package/out/internal/construction-claim.d.ts +1 -0
- package/out/internal/construction-claim.luau +26 -0
- package/out/internal/data/admission.d.ts +1 -0
- package/out/internal/data/admission.luau +673 -0
- package/out/internal/data/declarations.d.ts +1 -0
- package/out/internal/data/declarations.luau +384 -0
- package/out/internal/data/envelope.d.ts +1 -0
- package/out/internal/data/envelope.luau +318 -0
- package/out/internal/data/handles.d.ts +1 -0
- package/out/internal/data/handles.luau +255 -0
- package/out/internal/data/inbox-transforms.d.ts +1 -0
- package/out/internal/data/inbox-transforms.luau +458 -0
- package/out/internal/data/inbox.d.ts +1 -0
- package/out/internal/data/inbox.luau +437 -0
- package/out/internal/data/lease.d.ts +1 -0
- package/out/internal/data/lease.luau +304 -0
- package/out/internal/data/limits.d.ts +1 -0
- package/out/internal/data/limits.luau +234 -0
- package/out/internal/data/messages.d.ts +1 -0
- package/out/internal/data/messages.luau +69 -0
- package/out/internal/data/operation-id.d.ts +1 -0
- package/out/internal/data/operation-id.luau +61 -0
- package/out/internal/data/paths.d.ts +2 -0
- package/out/internal/data/paths.luau +323 -0
- package/out/internal/data/port.d.ts +1 -0
- package/out/internal/data/port.luau +290 -0
- package/out/internal/data/profiles.d.ts +1 -0
- package/out/internal/data/profiles.luau +862 -0
- package/out/internal/data/receipts.d.ts +1 -0
- package/out/internal/data/receipts.luau +190 -0
- package/out/internal/data/record-bindings.d.ts +1 -0
- package/out/internal/data/record-bindings.luau +92 -0
- package/out/internal/data/record-declarations.d.ts +1 -0
- package/out/internal/data/record-declarations.luau +140 -0
- package/out/internal/data/records.d.ts +1 -0
- package/out/internal/data/records.luau +400 -0
- package/out/internal/data/runtime.d.ts +1 -0
- package/out/internal/data/runtime.luau +918 -0
- package/out/internal/data/scheduler.d.ts +1 -0
- package/out/internal/data/scheduler.luau +507 -0
- package/out/internal/data/session.d.ts +1 -0
- package/out/internal/data/session.luau +234 -0
- package/out/internal/data/shutdown.d.ts +1 -0
- package/out/internal/data/shutdown.luau +91 -0
- package/out/internal/data/waiters.d.ts +1 -0
- package/out/internal/data/waiters.luau +65 -0
- package/out/internal/dense-list.d.ts +1 -0
- package/out/internal/dense-list.luau +155 -0
- package/out/internal/diagnostics.d.ts +1 -0
- package/out/internal/diagnostics.luau +29 -0
- package/out/internal/hooks.d.ts +1 -0
- package/out/internal/hooks.luau +53 -0
- package/out/internal/implementers.d.ts +1 -0
- package/out/internal/implementers.luau +119 -0
- package/out/internal/lifecycle.d.ts +1 -0
- package/out/internal/lifecycle.luau +912 -0
- package/out/internal/log-output.d.ts +1 -0
- package/out/internal/log-output.luau +106 -0
- package/out/internal/network/catalog.d.ts +26 -0
- package/out/internal/network/catalog.luau +182 -0
- package/out/internal/network/claims.d.ts +1 -0
- package/out/internal/network/claims.luau +154 -0
- package/out/internal/network/client-session.d.ts +1 -0
- package/out/internal/network/client-session.luau +403 -0
- package/out/internal/network/codec.d.ts +63 -0
- package/out/internal/network/codec.luau +1183 -0
- package/out/internal/network/envelope.d.ts +1 -0
- package/out/internal/network/envelope.luau +247 -0
- package/out/internal/network/limits.d.ts +26 -0
- package/out/internal/network/limits.luau +55 -0
- package/out/internal/network/port.d.ts +1 -0
- package/out/internal/network/port.luau +19 -0
- package/out/internal/network/roblox-network-port.d.ts +1 -0
- package/out/internal/network/roblox-network-port.luau +105 -0
- package/out/internal/network/schema.d.ts +72 -0
- package/out/internal/network/schema.luau +386 -0
- package/out/internal/network/server-session.d.ts +1 -0
- package/out/internal/network/server-session.luau +393 -0
- package/out/internal/network/session.d.ts +1 -0
- package/out/internal/network/session.luau +266 -0
- package/out/internal/port.d.ts +1 -0
- package/out/internal/port.luau +12 -0
- package/out/internal/reactive/context.d.ts +1 -0
- package/out/internal/reactive/context.luau +149 -0
- package/out/internal/reactive/flush.d.ts +1 -0
- package/out/internal/reactive/flush.luau +355 -0
- package/out/internal/reactive/graph.d.ts +1 -0
- package/out/internal/reactive/graph.luau +626 -0
- package/out/internal/reactive/limits.d.ts +1 -0
- package/out/internal/reactive/limits.luau +54 -0
- package/out/internal/reactive/ownership.d.ts +1 -0
- package/out/internal/reactive/ownership.luau +626 -0
- package/out/internal/registry.d.ts +1 -0
- package/out/internal/registry.luau +455 -0
- package/out/internal/roblox-port.d.ts +1 -0
- package/out/internal/roblox-port.luau +117 -0
- package/out/internal/runner.d.ts +1 -0
- package/out/internal/runner.luau +86 -0
- package/out/internal/scheduler.d.ts +1 -0
- package/out/internal/scheduler.luau +207 -0
- package/out/internal/state.d.ts +1 -0
- package/out/internal/state.luau +41 -0
- package/out/internal/sync/client-sync.d.ts +1 -0
- package/out/internal/sync/client-sync.luau +157 -0
- package/out/internal/sync/declarations.d.ts +1 -0
- package/out/internal/sync/declarations.luau +276 -0
- package/out/internal/sync/frame.d.ts +1 -0
- package/out/internal/sync/frame.luau +91 -0
- package/out/internal/sync/server-sync.d.ts +1 -0
- package/out/internal/sync/server-sync.luau +502 -0
- package/out/internal/sync/values.d.ts +1 -0
- package/out/internal/sync/values.luau +930 -0
- package/out/log-level.d.ts +7 -0
- package/out/log-level.luau +21 -0
- package/out/logging.d.ts +34 -0
- package/out/logging.luau +233 -0
- package/out/network.d.ts +66 -0
- package/out/network.luau +24 -0
- package/out/reactive.d.ts +76 -0
- package/out/reactive.luau +136 -0
- package/out/replicated.d.ts +38 -0
- package/out/replicated.luau +45 -0
- package/out/server/data.d.ts +54 -0
- package/out/server/data.luau +44 -0
- package/out/server/index.d.ts +18 -0
- package/out/server/init.luau +62 -0
- package/out/server/publish.d.ts +20 -0
- package/out/server/publish.luau +24 -0
- package/out/subsystem.d.ts +4 -0
- package/out/subsystem.luau +30 -0
- package/out/vide/index.d.ts +7 -0
- package/out/vide/init.luau +50 -0
- package/package.json +52 -5
- package/server/package.json +4 -0
- package/transformer/data.cjs +72 -0
- package/transformer/declarations.cjs +153 -0
- package/transformer/diagnostics.cjs +19 -0
- package/transformer/index.js +741 -0
- package/transformer/network.cjs +961 -0
- package/transformer/type-id.cjs +80 -0
- package/vide/package.json +4 -0
- package/README.md +0 -3
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Migration, OperationId, ProfileMessage, StoreFailure, StoreResult } from "../data";
|
|
2
|
+
declare const recordKind: unique symbol;
|
|
3
|
+
export interface StoreOptions<T> {
|
|
4
|
+
/**
|
|
5
|
+
* A new record's value, and the value of a missing required key on an object path from the root. It never fills an
|
|
6
|
+
* array element, a map value or an optional key of an existing record. Admitted and frozen once.
|
|
7
|
+
*/
|
|
8
|
+
readonly defaults: T;
|
|
9
|
+
/** Append-only. Removing or reordering a step changes what every older record becomes. */
|
|
10
|
+
readonly migrations?: readonly Migration[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A generic update's result. `replayed` is true when the operation id was already recorded for the key: `change` did
|
|
14
|
+
* not run, and `value` is the current stored record (the defaults when the key holds no payload), which may include
|
|
15
|
+
* later writers' changes. A marker stores no result, so the original one cannot be returned.
|
|
16
|
+
*/
|
|
17
|
+
export type UpdateResult<T> = {
|
|
18
|
+
readonly ok: true;
|
|
19
|
+
readonly value: T;
|
|
20
|
+
readonly replayed: boolean;
|
|
21
|
+
} | {
|
|
22
|
+
readonly ok: false;
|
|
23
|
+
readonly reason: StoreFailure;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Keyed records with no session. Each call is one DataStore request that yields and may fail, so the surface is two
|
|
27
|
+
* calls with result unions, not live handles.
|
|
28
|
+
*/
|
|
29
|
+
export interface DataStore<T> {
|
|
30
|
+
readonly [recordKind]: {
|
|
31
|
+
readonly kind: "records";
|
|
32
|
+
readonly value: (value: T) => T;
|
|
33
|
+
};
|
|
34
|
+
/** Yields. The stored record, migrated and admitted, or undefined when the key has none. */
|
|
35
|
+
read(key: string): StoreResult<T | undefined>;
|
|
36
|
+
/**
|
|
37
|
+
* Yields. One atomic change; `change` may run more than once, so it must be pure. A replayed id returns the current
|
|
38
|
+
* record with `replayed: true`. A throw in `change` cancels the write and is rethrown here.
|
|
39
|
+
*/
|
|
40
|
+
update(key: string, change: (current: T) => T, id?: OperationId): UpdateResult<T>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Declares keyed records by their DataStore name. It must initialize an exported top-level const in a server module,
|
|
44
|
+
* with `T` explicit. `T` is an object or a string-keyed record at its root.
|
|
45
|
+
*/
|
|
46
|
+
export declare function dataStore<T extends object>(_name: string, _options: StoreOptions<NoInfer<T>>): DataStore<T>;
|
|
47
|
+
/** A fresh operation identity. Create it once per logical operation and reuse it for that operation's retries. */
|
|
48
|
+
export declare function operationId(): OperationId;
|
|
49
|
+
/**
|
|
50
|
+
* Declares a durable message to a player's profile. It must initialize an exported top-level const in a server module,
|
|
51
|
+
* with the payload type explicit. A payload type changes only by adding optional fields.
|
|
52
|
+
*/
|
|
53
|
+
export declare function profileMessage<P>(_name: string): ProfileMessage<P>;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local newOperationId = TS.import(script, script.Parent.Parent, "internal", "data", "operation-id").newOperationId
|
|
4
|
+
local dataPort = TS.import(script, script.Parent.Parent, "internal", "data", "port").dataPort
|
|
5
|
+
--[[
|
|
6
|
+
*
|
|
7
|
+
* A generic update's result. `replayed` is true when the operation id was already recorded for the key: `change` did
|
|
8
|
+
* not run, and `value` is the current stored record (the defaults when the key holds no payload), which may include
|
|
9
|
+
* later writers' changes. A marker stores no result, so the original one cannot be returned.
|
|
10
|
+
|
|
11
|
+
]]
|
|
12
|
+
--[[
|
|
13
|
+
*
|
|
14
|
+
* Keyed records with no session. Each call is one DataStore request that yields and may fail, so the surface is two
|
|
15
|
+
* calls with result unions, not live handles.
|
|
16
|
+
|
|
17
|
+
]]
|
|
18
|
+
--[[
|
|
19
|
+
*
|
|
20
|
+
* Declares keyed records by their DataStore name. It must initialize an exported top-level const in a server module,
|
|
21
|
+
* with `T` explicit. `T` is an object or a string-keyed record at its root.
|
|
22
|
+
|
|
23
|
+
]]
|
|
24
|
+
local function dataStore(_name, _options)
|
|
25
|
+
error("Crucible data needs the @trioforge/crucible/transformer plugin")
|
|
26
|
+
end
|
|
27
|
+
--* A fresh operation identity. Create it once per logical operation and reuse it for that operation's retries.
|
|
28
|
+
local function operationId()
|
|
29
|
+
return newOperationId(dataPort())
|
|
30
|
+
end
|
|
31
|
+
--[[
|
|
32
|
+
*
|
|
33
|
+
* Declares a durable message to a player's profile. It must initialize an exported top-level const in a server module,
|
|
34
|
+
* with the payload type explicit. A payload type changes only by adding optional fields.
|
|
35
|
+
|
|
36
|
+
]]
|
|
37
|
+
local function profileMessage(_name)
|
|
38
|
+
error("Crucible data needs the @trioforge/crucible/transformer plugin")
|
|
39
|
+
end
|
|
40
|
+
return {
|
|
41
|
+
dataStore = dataStore,
|
|
42
|
+
operationId = operationId,
|
|
43
|
+
profileMessage = profileMessage,
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BootOptions } from "../index";
|
|
2
|
+
import type { PayloadArgs, ToClient } from "../network";
|
|
3
|
+
import "../internal/data/record-bindings";
|
|
4
|
+
export { config, playerConfig, configReady, refreshConfigs } from "../configs";
|
|
5
|
+
export type { Config, PlayerConfig } from "../configs";
|
|
6
|
+
export { publish } from "./publish";
|
|
7
|
+
export type { Publication } from "./publish";
|
|
8
|
+
export { dataStore, operationId, profileMessage } from "./data";
|
|
9
|
+
export type { DataStore, StoreOptions, UpdateResult } from "./data";
|
|
10
|
+
export type { OperationId, StoreFailure, StoreResult } from "../data";
|
|
11
|
+
export type { ProfileMessage, ProfileClaim, LoadReason, ReleaseReason, Purchase, AfterGrant, QuarantinedMessage, SaveResult, SaveFailure, } from "../data";
|
|
12
|
+
export declare function boot(options: BootOptions): void;
|
|
13
|
+
export declare namespace Network {
|
|
14
|
+
/** True when accepted locally: fired now, or queued before the player's client is ready. Never delivery. */
|
|
15
|
+
function send<P>(player: Player, message: ToClient<P>, ...payload: PayloadArgs<NoInfer<P>>): boolean;
|
|
16
|
+
/** Encodes once and delivers the same packet to every player in the game now. */
|
|
17
|
+
function broadcast<P>(message: ToClient<P>, ...payload: PayloadArgs<NoInfer<P>>): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local exports = {}
|
|
4
|
+
local bootSide = TS.import(script, script.Parent, "internal", "boot").boot
|
|
5
|
+
local installConfigRuntime = TS.import(script, script.Parent, "internal", "configs", "runtime").installConfigRuntime
|
|
6
|
+
local networkPort = TS.import(script, script.Parent, "internal", "network", "port").networkPort
|
|
7
|
+
local ServerSession = TS.import(script, script.Parent, "internal", "network", "server-session").ServerSession
|
|
8
|
+
local SyncFrameMessage = TS.import(script, script.Parent, "internal", "sync", "frame").SyncFrameMessage
|
|
9
|
+
local installServerSync = TS.import(script, script.Parent, "internal", "sync", "server-sync").installServerSync
|
|
10
|
+
TS.import(script, script.Parent, "internal", "data", "record-bindings")
|
|
11
|
+
local installDataRuntime = TS.import(script, script.Parent, "internal", "data", "runtime").installDataRuntime
|
|
12
|
+
if not game:GetService("RunService"):IsServer() then
|
|
13
|
+
error("@trioforge/crucible/server loaded on the client")
|
|
14
|
+
end
|
|
15
|
+
local session
|
|
16
|
+
local _configs = TS.import(script, script.Parent, "configs")
|
|
17
|
+
exports.config = _configs.config
|
|
18
|
+
exports.playerConfig = _configs.playerConfig
|
|
19
|
+
exports.configReady = _configs.configReady
|
|
20
|
+
exports.refreshConfigs = _configs.refreshConfigs
|
|
21
|
+
exports.publish = TS.import(script, script, "publish").publish
|
|
22
|
+
local _data = TS.import(script, script, "data")
|
|
23
|
+
exports.dataStore = _data.dataStore
|
|
24
|
+
exports.operationId = _data.operationId
|
|
25
|
+
exports.profileMessage = _data.profileMessage
|
|
26
|
+
local function active()
|
|
27
|
+
return session or error("Crucible networking is not ready; send from a constructor, a hook, or later", 0)
|
|
28
|
+
end
|
|
29
|
+
installConfigRuntime()
|
|
30
|
+
installDataRuntime()
|
|
31
|
+
local function boot(options)
|
|
32
|
+
local network = ServerSession.new(networkPort())
|
|
33
|
+
session = network
|
|
34
|
+
installServerSync(function(player, frame)
|
|
35
|
+
return active():send(player, SyncFrameMessage, frame)
|
|
36
|
+
end)
|
|
37
|
+
bootSide({
|
|
38
|
+
name = "server",
|
|
39
|
+
renders = false,
|
|
40
|
+
booted = function() end,
|
|
41
|
+
network = network,
|
|
42
|
+
}, options)
|
|
43
|
+
end
|
|
44
|
+
local Network = {}
|
|
45
|
+
do
|
|
46
|
+
local _container = Network
|
|
47
|
+
--* True when accepted locally: fired now, or queued before the player's client is ready. Never delivery.
|
|
48
|
+
local function send(player, message, ...)
|
|
49
|
+
local payload = { ... }
|
|
50
|
+
return active():send(player, message, payload[1])
|
|
51
|
+
end
|
|
52
|
+
_container.send = send
|
|
53
|
+
--* Encodes once and delivers the same packet to every player in the game now.
|
|
54
|
+
local function broadcast(message, ...)
|
|
55
|
+
local payload = { ... }
|
|
56
|
+
active():broadcast(message, payload[1])
|
|
57
|
+
end
|
|
58
|
+
_container.broadcast = broadcast
|
|
59
|
+
end
|
|
60
|
+
exports.boot = boot
|
|
61
|
+
exports.Network = Network
|
|
62
|
+
return exports
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { State } from "../reactive";
|
|
2
|
+
import type { Replicated, ReplicatedPerPlayer } from "../replicated";
|
|
3
|
+
declare const publication: unique symbol;
|
|
4
|
+
/** What a per-player `publish` returns: an opaque claim that holds nothing callable. */
|
|
5
|
+
export interface Publication {
|
|
6
|
+
readonly [publication]: true;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Takes the write side of a value every client sees. Call it while a server Subsystem is constructed, as a field
|
|
10
|
+
* initializer: `private readonly phase = publish(JobPhase)`. `set` and `update` drop fields the type does not
|
|
11
|
+
* declare, check and freeze the rest, and throw at a write that cannot replicate. One Subsystem publishes each value.
|
|
12
|
+
*/
|
|
13
|
+
export declare function publish<T>(target: Replicated<T>): State<T>;
|
|
14
|
+
/**
|
|
15
|
+
* Supplies a per-player value as a projection. For each player whose client is ready, it is evaluated like a
|
|
16
|
+
* `computed`, admitted on every pull and sent to that player alone. It must be pure and must not yield. A throw or a
|
|
17
|
+
* rejected value marks that player's value failed until it next changes.
|
|
18
|
+
*/
|
|
19
|
+
export declare function publish<T>(target: ReplicatedPerPlayer<T>, project: (player: Player) => T): Publication;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local claimPublication = TS.import(script, script.Parent.Parent, "internal", "sync", "declarations").claimPublication
|
|
4
|
+
--* What a per-player `publish` returns: an opaque claim that holds nothing callable.
|
|
5
|
+
--[[
|
|
6
|
+
*
|
|
7
|
+
* Takes the write side of a value every client sees. Call it while a server Subsystem is constructed, as a field
|
|
8
|
+
* initializer: `private readonly phase = publish(JobPhase)`. `set` and `update` drop fields the type does not
|
|
9
|
+
* declare, check and freeze the rest, and throw at a write that cannot replicate. One Subsystem publishes each value.
|
|
10
|
+
|
|
11
|
+
]]
|
|
12
|
+
--[[
|
|
13
|
+
*
|
|
14
|
+
* Supplies a per-player value as a projection. For each player whose client is ready, it is evaluated like a
|
|
15
|
+
* `computed`, admitted on every pull and sent to that player alone. It must be pure and must not yield. A throw or a
|
|
16
|
+
* rejected value marks that player's value failed until it next changes.
|
|
17
|
+
|
|
18
|
+
]]
|
|
19
|
+
local function publish(target, project)
|
|
20
|
+
return claimPublication(target, project)
|
|
21
|
+
end
|
|
22
|
+
return {
|
|
23
|
+
publish = publish,
|
|
24
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local CrucibleObject = TS.import(script, script.Parent, "crucible-object").CrucibleObject
|
|
4
|
+
local installSubsystemHooks = TS.import(script, script.Parent, "internal", "hooks").installSubsystemHooks
|
|
5
|
+
local Subsystem
|
|
6
|
+
do
|
|
7
|
+
local super = CrucibleObject
|
|
8
|
+
Subsystem = setmetatable({}, {
|
|
9
|
+
__tostring = function()
|
|
10
|
+
return "Subsystem"
|
|
11
|
+
end,
|
|
12
|
+
__index = super,
|
|
13
|
+
})
|
|
14
|
+
Subsystem.__index = Subsystem
|
|
15
|
+
function Subsystem:constructor(...)
|
|
16
|
+
super.constructor(self, ...)
|
|
17
|
+
end
|
|
18
|
+
function Subsystem:onShutdown()
|
|
19
|
+
end
|
|
20
|
+
do
|
|
21
|
+
installSubsystemHooks({
|
|
22
|
+
onShutdown = function(o)
|
|
23
|
+
return o:onShutdown()
|
|
24
|
+
end,
|
|
25
|
+
})
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
return {
|
|
29
|
+
Subsystem = Subsystem,
|
|
30
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Readable } from "../reactive";
|
|
2
|
+
/**
|
|
3
|
+
* A Vide source that follows `readable`. It holds the value read now, untracked, and takes each change on the
|
|
4
|
+
* reactive flush, through a `watch` that ends with the current Vide owner. Outside a Vide component or `root` it
|
|
5
|
+
* throws.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useReadable<T>(readable: Readable<T>): () => T;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local _vide = TS.import(script, TS.getModule(script, "@rbxts", "vide").src)
|
|
4
|
+
local cleanup = _vide.cleanup
|
|
5
|
+
local source = _vide.source
|
|
6
|
+
local untracked = TS.import(script, script.Parent, "reactive").untracked
|
|
7
|
+
local createWatchFrom = TS.import(script, script.Parent, "internal", "reactive", "flush").createWatchFrom
|
|
8
|
+
local createRootScope = TS.import(script, script.Parent, "internal", "reactive", "ownership").createRootScope
|
|
9
|
+
local NO_OWNER = "useReadable() needs a Vide owner: call it inside a component or root()"
|
|
10
|
+
--[[
|
|
11
|
+
*
|
|
12
|
+
* A Vide source that follows `readable`. It holds the value read now, untracked, and takes each change on the
|
|
13
|
+
* reactive flush, through a `watch` that ends with the current Vide owner. Outside a Vide component or `root` it
|
|
14
|
+
* throws.
|
|
15
|
+
|
|
16
|
+
]]
|
|
17
|
+
local function useReadable(readable)
|
|
18
|
+
local dispose
|
|
19
|
+
-- Vide's cleanup is the only public test for an owner, and registering first leaves nothing to undo.
|
|
20
|
+
local owned = pcall(cleanup, function()
|
|
21
|
+
local _result = dispose
|
|
22
|
+
if _result ~= nil then
|
|
23
|
+
_result = _result()
|
|
24
|
+
end
|
|
25
|
+
return _result
|
|
26
|
+
end)
|
|
27
|
+
if not owned then
|
|
28
|
+
error(NO_OWNER, 2)
|
|
29
|
+
end
|
|
30
|
+
local initial = untracked(readable)
|
|
31
|
+
local value = source(initial)
|
|
32
|
+
local latest = initial
|
|
33
|
+
local write = function()
|
|
34
|
+
value(latest)
|
|
35
|
+
return nil
|
|
36
|
+
end
|
|
37
|
+
-- A root, so a component mounted from inside a Crucible run or callback does not end with it.
|
|
38
|
+
dispose = createRootScope(function()
|
|
39
|
+
return createWatchFrom(readable, initial, function(current)
|
|
40
|
+
latest = current
|
|
41
|
+
-- Vide runs its effects inside the write. Across gsub's C call a yield in one throws where it yields, so
|
|
42
|
+
-- Vide's own error path pops its scope stack. Without it the runner cancels the yielded worker mid-effect.
|
|
43
|
+
string.gsub("x", "x", write)
|
|
44
|
+
end)
|
|
45
|
+
end)
|
|
46
|
+
return value
|
|
47
|
+
end
|
|
48
|
+
return {
|
|
49
|
+
useReadable = useReadable,
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@trioforge/crucible",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"main": "out/init.luau",
|
|
6
|
+
"types": "out/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./out/index.d.ts",
|
|
10
|
+
"default": "./out/init.luau"
|
|
11
|
+
},
|
|
12
|
+
"./server": {
|
|
13
|
+
"types": "./out/server/index.d.ts",
|
|
14
|
+
"default": "./out/server/init.luau"
|
|
15
|
+
},
|
|
16
|
+
"./emit": {
|
|
17
|
+
"types": "./out/emit/index.d.ts",
|
|
18
|
+
"default": "./out/emit/init.luau"
|
|
19
|
+
},
|
|
20
|
+
"./client": {
|
|
21
|
+
"types": "./out/client/index.d.ts",
|
|
22
|
+
"default": "./out/client/init.luau"
|
|
23
|
+
},
|
|
24
|
+
"./vide": {
|
|
25
|
+
"types": "./out/vide/index.d.ts",
|
|
26
|
+
"default": "./out/vide/init.luau"
|
|
27
|
+
},
|
|
28
|
+
"./transformer": "./transformer/index.js"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"typescript": "5.5.3",
|
|
32
|
+
"@rbxts/vide": "^0.6.1"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"@rbxts/vide": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"out",
|
|
41
|
+
"transformer",
|
|
42
|
+
"server",
|
|
43
|
+
"client",
|
|
44
|
+
"emit",
|
|
45
|
+
"vide",
|
|
46
|
+
"LICENSE"
|
|
47
|
+
],
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/Trio-Forge-Studio/crucible.git",
|
|
51
|
+
"directory": "packages/crucible"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
function createDataChecks({ ts, program, checker, api }) {
|
|
2
|
+
const unwrap = (expression) => {
|
|
3
|
+
while (ts.isParenthesizedExpression(expression) || ts.isNonNullExpression(expression))
|
|
4
|
+
expression = expression.expression;
|
|
5
|
+
return expression;
|
|
6
|
+
};
|
|
7
|
+
const member = (expression) => {
|
|
8
|
+
if (ts.isPropertyAccessExpression(expression)) return expression.name.text;
|
|
9
|
+
if (ts.isElementAccessExpression(expression) && ts.isStringLiteralLike(expression.argumentExpression))
|
|
10
|
+
return expression.argumentExpression.text;
|
|
11
|
+
};
|
|
12
|
+
const store = (expression) => api(checker.getTypeAtLocation(expression).getSymbol()) === "ProfileStore";
|
|
13
|
+
const marketplace = (expression) => {
|
|
14
|
+
const symbol = checker.getTypeAtLocation(expression).getSymbol();
|
|
15
|
+
return (
|
|
16
|
+
symbol?.name === "MarketplaceService" &&
|
|
17
|
+
symbol.declarations?.some((d) =>
|
|
18
|
+
d.getSourceFile().fileName.replaceAll("\\", "/").includes("/@rbxts/types/"),
|
|
19
|
+
)
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
const storeCall = (node) => {
|
|
23
|
+
const callee = unwrap(node.expression);
|
|
24
|
+
if (store(callee)) return { store: callee };
|
|
25
|
+
const name = member(callee);
|
|
26
|
+
if (name !== undefined && store(callee.expression)) return { store: callee.expression, member: name };
|
|
27
|
+
};
|
|
28
|
+
let granted;
|
|
29
|
+
const grants = () => {
|
|
30
|
+
if (granted === undefined) {
|
|
31
|
+
granted = false;
|
|
32
|
+
const walk = (node) => {
|
|
33
|
+
if (granted) return;
|
|
34
|
+
if (ts.isCallExpression(node) && storeCall(node)?.member === "grant") granted = true;
|
|
35
|
+
else ts.forEachChild(node, walk);
|
|
36
|
+
};
|
|
37
|
+
for (const source of program.getSourceFiles()) if (!source.isDeclarationFile) walk(source);
|
|
38
|
+
}
|
|
39
|
+
return granted;
|
|
40
|
+
};
|
|
41
|
+
return ({ report, side }) =>
|
|
42
|
+
(node) => {
|
|
43
|
+
if (ts.isCallExpression(node)) {
|
|
44
|
+
const call = storeCall(node);
|
|
45
|
+
if (call) {
|
|
46
|
+
const text = call.store.getText();
|
|
47
|
+
const client =
|
|
48
|
+
call.member === undefined || call.member === "loaded" ? !node.arguments.length : false;
|
|
49
|
+
const required = client ? "client" : "server";
|
|
50
|
+
if (side(node) !== required)
|
|
51
|
+
report(
|
|
52
|
+
node,
|
|
53
|
+
"CRU027",
|
|
54
|
+
client
|
|
55
|
+
? `${text}${call.member ? ".loaded" : ""}() reads the local player's profile; on the server call ${text}${call.member ? ".loaded" : ""}(player).`
|
|
56
|
+
: `${text}${call.member === undefined ? "(player)" : `.${call.member}`} is server-only; the client reads ${text}().`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
61
|
+
const target = unwrap(node.left);
|
|
62
|
+
if (member(target) === "ProcessReceipt" && marketplace(target.expression) && grants())
|
|
63
|
+
report(
|
|
64
|
+
node,
|
|
65
|
+
"CRU025",
|
|
66
|
+
"Crucible's data module owns ProcessReceipt; register grants with a profile store's grant(productId, ...).",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { createDataChecks };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const limits = Object.freeze({
|
|
2
|
+
MESSAGE_NAME_MAX_BYTES: 64,
|
|
3
|
+
MAX_ASK_TIMEOUT_SECONDS: 120,
|
|
4
|
+
MAX_UNION_BRANCHES: 256,
|
|
5
|
+
MAX_SCHEMA_NODES: 4096,
|
|
6
|
+
MAX_DEPTH: 32,
|
|
7
|
+
});
|
|
8
|
+
const reactiveLimits = Object.freeze({
|
|
9
|
+
MESSAGE_NAME_MAX_BYTES: 64,
|
|
10
|
+
SYNC_VALUE_DEPTH: 28,
|
|
11
|
+
});
|
|
12
|
+
const dataLimits = Object.freeze({
|
|
13
|
+
STORE_NAME_MAX_BYTES: 50,
|
|
14
|
+
MESSAGE_NAME_MAX_BYTES: 64,
|
|
15
|
+
CONFIG_NAME_MAX_BYTES: 64,
|
|
16
|
+
PERSIST_DEPTH: 28,
|
|
17
|
+
VIEW_NAME_PREFIX: "Crucible.Data.",
|
|
18
|
+
LIVE_NAME_PREFIX: "Crucible.Live.",
|
|
19
|
+
VIEW_NAME_MAX_BYTES: 64,
|
|
20
|
+
RESERVED_HANDLE_NAMES: Object.freeze(["set", "update", "insert", "remove"]),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const message = {
|
|
24
|
+
label: "messages",
|
|
25
|
+
side: "shared",
|
|
26
|
+
names: "Message",
|
|
27
|
+
maxBytes: limits.MESSAGE_NAME_MAX_BYTES,
|
|
28
|
+
typeArguments: 1,
|
|
29
|
+
arguments: 1,
|
|
30
|
+
passed: [],
|
|
31
|
+
position: "payload",
|
|
32
|
+
};
|
|
33
|
+
const replicated = {
|
|
34
|
+
label: "replicated state",
|
|
35
|
+
side: "shared",
|
|
36
|
+
names: "Replicated",
|
|
37
|
+
maxBytes: reactiveLimits.MESSAGE_NAME_MAX_BYTES,
|
|
38
|
+
typeArguments: 1,
|
|
39
|
+
arguments: 2,
|
|
40
|
+
passed: [1],
|
|
41
|
+
position: "state",
|
|
42
|
+
};
|
|
43
|
+
const config = {
|
|
44
|
+
label: "configs",
|
|
45
|
+
side: "server",
|
|
46
|
+
names: "Config",
|
|
47
|
+
maxBytes: dataLimits.CONFIG_NAME_MAX_BYTES,
|
|
48
|
+
typeArguments: 1,
|
|
49
|
+
arguments: 2,
|
|
50
|
+
passed: [1],
|
|
51
|
+
position: "persist",
|
|
52
|
+
};
|
|
53
|
+
const kinds = Object.freeze({
|
|
54
|
+
toServer: message,
|
|
55
|
+
toClient: message,
|
|
56
|
+
askServer: { ...message, typeArguments: 2, arguments: 2 },
|
|
57
|
+
replicated,
|
|
58
|
+
replicatedPerPlayer: replicated,
|
|
59
|
+
profileStore: {
|
|
60
|
+
label: "profile stores",
|
|
61
|
+
side: "shared",
|
|
62
|
+
names: "Store",
|
|
63
|
+
maxBytes: dataLimits.STORE_NAME_MAX_BYTES,
|
|
64
|
+
typeArguments: "optional",
|
|
65
|
+
arguments: 2,
|
|
66
|
+
passed: [1],
|
|
67
|
+
position: "persist",
|
|
68
|
+
store: true,
|
|
69
|
+
handles: true,
|
|
70
|
+
},
|
|
71
|
+
dataStore: {
|
|
72
|
+
label: "data stores",
|
|
73
|
+
side: "server",
|
|
74
|
+
names: "Store",
|
|
75
|
+
maxBytes: dataLimits.STORE_NAME_MAX_BYTES,
|
|
76
|
+
typeArguments: 1,
|
|
77
|
+
arguments: 2,
|
|
78
|
+
passed: [1],
|
|
79
|
+
position: "persist",
|
|
80
|
+
store: true,
|
|
81
|
+
},
|
|
82
|
+
profileMessage: {
|
|
83
|
+
label: "profile messages",
|
|
84
|
+
side: "server",
|
|
85
|
+
names: "Profile message",
|
|
86
|
+
maxBytes: dataLimits.MESSAGE_NAME_MAX_BYTES,
|
|
87
|
+
typeArguments: 1,
|
|
88
|
+
arguments: 1,
|
|
89
|
+
passed: [],
|
|
90
|
+
position: "persist",
|
|
91
|
+
},
|
|
92
|
+
config,
|
|
93
|
+
playerConfig: config,
|
|
94
|
+
});
|
|
95
|
+
const depthLimits = Object.freeze({
|
|
96
|
+
payload: limits.MAX_DEPTH,
|
|
97
|
+
state: reactiveLimits.SYNC_VALUE_DEPTH,
|
|
98
|
+
persist: dataLimits.PERSIST_DEPTH,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Marks sit only on object fields from the root, so only object fields can lose every field.
|
|
102
|
+
function replica(node) {
|
|
103
|
+
if (node.kind !== "object") return node;
|
|
104
|
+
const fields = [];
|
|
105
|
+
for (const [key, child] of node.fields) {
|
|
106
|
+
if (child.serverOnly) continue;
|
|
107
|
+
const kept = replica(child);
|
|
108
|
+
if (kept.kind === "object" && !kept.fields.length) continue;
|
|
109
|
+
fields.push([key, kept]);
|
|
110
|
+
}
|
|
111
|
+
return { kind: "object", fields };
|
|
112
|
+
}
|
|
113
|
+
// Object fields are sorted by bytes, so a depth-first walk lists paths in rule 131 order, a prefix first.
|
|
114
|
+
function serverOnlyPaths(node, prefix = []) {
|
|
115
|
+
if (node.kind !== "object") return [];
|
|
116
|
+
return node.fields.flatMap(([key, child]) =>
|
|
117
|
+
child.serverOnly ? [[...prefix, key]] : serverOnlyPaths(child, [...prefix, key]),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const SEGMENT = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
121
|
+
function viewNameProblems(store, node) {
|
|
122
|
+
if (node.kind !== "object") return [];
|
|
123
|
+
const prefix = `${dataLimits.VIEW_NAME_PREFIX}${store}.`;
|
|
124
|
+
return node.fields.flatMap(([field]) => {
|
|
125
|
+
const name = prefix + field;
|
|
126
|
+
if (!SEGMENT.test(field))
|
|
127
|
+
return [
|
|
128
|
+
[
|
|
129
|
+
`value[${JSON.stringify(field)}]`,
|
|
130
|
+
"a field the owner's client receives is named by one declaration segment, [A-Za-z][A-Za-z0-9_]*; rename it or mark it serverOnly",
|
|
131
|
+
],
|
|
132
|
+
];
|
|
133
|
+
if (Buffer.byteLength(name) > dataLimits.VIEW_NAME_MAX_BYTES)
|
|
134
|
+
return [
|
|
135
|
+
[
|
|
136
|
+
`value.${field}`,
|
|
137
|
+
`its view name ${name} is ${Buffer.byteLength(name)} bytes, over ${dataLimits.VIEW_NAME_MAX_BYTES}; this store allows field names of at most ${dataLimits.VIEW_NAME_MAX_BYTES - Buffer.byteLength(prefix)} bytes; rename the field, shorten the store name, or mark it serverOnly`,
|
|
138
|
+
],
|
|
139
|
+
];
|
|
140
|
+
return [];
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
limits,
|
|
146
|
+
reactiveLimits,
|
|
147
|
+
dataLimits,
|
|
148
|
+
kinds,
|
|
149
|
+
depthLimits,
|
|
150
|
+
replica,
|
|
151
|
+
serverOnlyPaths,
|
|
152
|
+
viewNameProblems,
|
|
153
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function createReporter(ts, addDiagnostic) {
|
|
2
|
+
const seen = new Set();
|
|
3
|
+
return (node, code, message) => {
|
|
4
|
+
const key = `${node.getSourceFile().fileName}:${node.getStart()}:${code}`;
|
|
5
|
+
if (seen.has(key)) return;
|
|
6
|
+
seen.add(key);
|
|
7
|
+
return addDiagnostic({
|
|
8
|
+
category: ts.DiagnosticCategory.Error,
|
|
9
|
+
code: 90000 + (/^CRU\d+$/.test(code) ? Number(code.slice(3)) : 0),
|
|
10
|
+
source: "crucible",
|
|
11
|
+
file: node.getSourceFile(),
|
|
12
|
+
start: node.getStart(),
|
|
13
|
+
length: node.getWidth(),
|
|
14
|
+
messageText: `${code}: ${message}`,
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { createReporter };
|