@pylonsync/sdk 0.11.6 → 0.13.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/dist/index.d.ts +117 -0
- package/package.json +1 -1
- package/src/build-config.test.ts +47 -0
- package/src/index.ts +220 -0
- package/src/shard-config.test.ts +40 -0
package/dist/index.d.ts
CHANGED
|
@@ -684,7 +684,120 @@ export interface AppManifest {
|
|
|
684
684
|
* `pylon deploy` refuses to ship when one is missing from the
|
|
685
685
|
* project's secrets. */
|
|
686
686
|
requiredEnv?: ManifestRequiredEnv[];
|
|
687
|
+
/** Production build settings (`pylon build`). */
|
|
688
|
+
build?: BuildConfig;
|
|
689
|
+
/** Realtime shard kinds whose simulation is a WebAssembly module. */
|
|
690
|
+
shards?: ManifestShard[];
|
|
687
691
|
}
|
|
692
|
+
/**
|
|
693
|
+
* Production build settings for `pylon build`. Dev (`pylon dev`) ignores the
|
|
694
|
+
* browser settings; they apply to the artifact and to `pylon start` builds.
|
|
695
|
+
*
|
|
696
|
+
* ```ts
|
|
697
|
+
* buildManifest({
|
|
698
|
+
* // ...
|
|
699
|
+
* build: {
|
|
700
|
+
* target: ["chrome >= 90", "safari >= 14", "firefox >= 90"],
|
|
701
|
+
* polyfill: "usage",
|
|
702
|
+
* server: { external: ["sharp"] },
|
|
703
|
+
* },
|
|
704
|
+
* });
|
|
705
|
+
* ```
|
|
706
|
+
*/
|
|
707
|
+
export interface BuildConfig {
|
|
708
|
+
/** Browser target for client JavaScript: an ECMAScript edition
|
|
709
|
+
* (`"es2018"` to `"es2022"`) or browserslist queries. Unset: modern
|
|
710
|
+
* output, no lowering. Needs `@swc/core` and `browserslist` in the app's
|
|
711
|
+
* devDependencies. The client runtime needs ES modules and dynamic
|
|
712
|
+
* `import()`, so browsers older than Chrome 63, Firefox 67, and Safari
|
|
713
|
+
* 11.1 cannot run it at any target. */
|
|
714
|
+
target?: string | string[];
|
|
715
|
+
/** core-js polyfills for `target`. `"usage"` adds the features the bundle
|
|
716
|
+
* uses; `"entry"` adds every feature the target lacks. Needs `core-js` in
|
|
717
|
+
* the app's dependencies. Default `false`. */
|
|
718
|
+
polyfill?: false | "usage" | "entry";
|
|
719
|
+
css?: {
|
|
720
|
+
/** CSS target (prefixes, nesting, color syntax), same forms as
|
|
721
|
+
* `target`. Default: `target`. Needs `lightningcss` and `browserslist`. */
|
|
722
|
+
target?: string | string[];
|
|
723
|
+
};
|
|
724
|
+
/** Emit source maps for the client and server bundles. Default `false`. */
|
|
725
|
+
sourcemap?: boolean;
|
|
726
|
+
server?: {
|
|
727
|
+
/** Bundle npm packages into the server output. `false` ships every
|
|
728
|
+
* dependency in `server/node_modules`. Default `true`. */
|
|
729
|
+
bundle?: boolean;
|
|
730
|
+
/** Packages to leave out of the server bundle and ship in
|
|
731
|
+
* `server/node_modules` with their dependencies: native modules and
|
|
732
|
+
* packages that read their own files at run time. */
|
|
733
|
+
external?: string[];
|
|
734
|
+
};
|
|
735
|
+
/** Project files or directories to copy into the artifact, for files the
|
|
736
|
+
* app reads from disk at run time (content/, data/). `public/` and image
|
|
737
|
+
* files under `app/` are always copied. */
|
|
738
|
+
include?: string[];
|
|
739
|
+
}
|
|
740
|
+
/** One realtime shard kind. See {@link shard}. */
|
|
741
|
+
export interface ManifestShard {
|
|
742
|
+
/** The kind name `ctx.shards.create(kind, id)` takes. */
|
|
743
|
+
name: string;
|
|
744
|
+
/** The compiled module, relative to the app root. */
|
|
745
|
+
wasm: string;
|
|
746
|
+
/** A Cargo crate, relative to the app root, that `pylon shards build`
|
|
747
|
+
* compiles to `wasm`. `pylon dev` runs that build at start. */
|
|
748
|
+
crate?: string;
|
|
749
|
+
/** Snapshot and input codec. Default `"json"`. */
|
|
750
|
+
codec?: "json" | "msgpack";
|
|
751
|
+
/** Ticks per second. `0` ticks only when inputs arrive. Default 20. */
|
|
752
|
+
tickRate?: number;
|
|
753
|
+
/** Pass `tick` a constant `1 / tickRate` instead of the measured time,
|
|
754
|
+
* so a replay gives the same result. Default `true`. */
|
|
755
|
+
fixedTimestep?: boolean;
|
|
756
|
+
/** Subscribers one shard admits. Default 256. */
|
|
757
|
+
maxSubscribers?: number;
|
|
758
|
+
/** Shards of this kind that may run at once. Default 64. */
|
|
759
|
+
maxInstances?: number;
|
|
760
|
+
/** Memory cap per shard, in MiB. Default 64. */
|
|
761
|
+
memoryMb?: number;
|
|
762
|
+
/** Time budget for one tick (its inputs, `tick`, and the snapshots) or
|
|
763
|
+
* one authorize call, in milliseconds. A tick that runs longer stops the
|
|
764
|
+
* shard. Default 100. */
|
|
765
|
+
tickBudgetMs?: number;
|
|
766
|
+
/** Stop a shard after this many seconds with no subscribers and no
|
|
767
|
+
* inputs. `0` never stops it. Default 90. */
|
|
768
|
+
idleShutdownSecs?: number;
|
|
769
|
+
/** Per-subscriber input limits. */
|
|
770
|
+
input?: {
|
|
771
|
+
/** Sustained inputs per second. Default 120. */
|
|
772
|
+
ratePerSec?: number;
|
|
773
|
+
/** Inputs above the rate a subscriber may send in a burst. Default 240. */
|
|
774
|
+
burst?: number;
|
|
775
|
+
/** Inputs one subscriber may have queued. Default 256. */
|
|
776
|
+
maxQueued?: number;
|
|
777
|
+
/** Inputs applied per subscriber per tick. Default 32. */
|
|
778
|
+
maxPerTick?: number;
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Declare a realtime shard kind whose simulation is a WebAssembly module.
|
|
783
|
+
* Build the module with the `pylon-shard-guest` Rust crate (or any
|
|
784
|
+
* toolchain that implements its ABI). The stock `pylon` binary runs it, on
|
|
785
|
+
* Pylon Cloud too.
|
|
786
|
+
*
|
|
787
|
+
* ```ts
|
|
788
|
+
* buildManifest({
|
|
789
|
+
* // ...
|
|
790
|
+
* shards: [
|
|
791
|
+
* shard({ name: "arena", wasm: "shards/arena.wasm", crate: "shards/arena", tickRate: 30 }),
|
|
792
|
+
* ],
|
|
793
|
+
* });
|
|
794
|
+
* ```
|
|
795
|
+
*
|
|
796
|
+
* A function starts a shard with `ctx.shards.create("arena", matchId, params)`
|
|
797
|
+
* and mints tickets with `ctx.shards.ticket(matchId)`. Clients connect with
|
|
798
|
+
* `useShard(matchId, { ticket })`.
|
|
799
|
+
*/
|
|
800
|
+
export declare function shard(def: ManifestShard): ManifestShard;
|
|
688
801
|
/** One environment variable the app declares it needs. */
|
|
689
802
|
export interface ManifestRequiredEnv {
|
|
690
803
|
name: string;
|
|
@@ -1121,6 +1234,10 @@ export declare function buildManifest(options: {
|
|
|
1121
1234
|
* ```
|
|
1122
1235
|
*/
|
|
1123
1236
|
requiredEnv?: ManifestRequiredEnv[];
|
|
1237
|
+
/** Production build settings for `pylon build`. See `BuildConfig`. */
|
|
1238
|
+
build?: BuildConfig;
|
|
1239
|
+
/** Realtime shard kinds. See `shard`. */
|
|
1240
|
+
shards?: ManifestShard[];
|
|
1124
1241
|
/** Set by `discoverFunctions()` (spread its result into this call).
|
|
1125
1242
|
* When true, the framework's AgentRun/AgentMessage entities and
|
|
1126
1243
|
* their owner-scoping policies are appended to the manifest —
|
package/package.json
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `buildManifest({ build })`: the production build block reaches the
|
|
3
|
+
* manifest as written, and a value `pylon build` cannot use fails when app.ts
|
|
4
|
+
* runs, not at build time.
|
|
5
|
+
*/
|
|
6
|
+
import { expect, test } from "bun:test";
|
|
7
|
+
import { buildManifest, entity, field } from "./index";
|
|
8
|
+
|
|
9
|
+
const base = {
|
|
10
|
+
name: "t",
|
|
11
|
+
version: "0",
|
|
12
|
+
entities: [entity("Doc", { title: field.string() })],
|
|
13
|
+
routes: [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
test("the build block is copied into the manifest", () => {
|
|
17
|
+
const build = {
|
|
18
|
+
target: ["chrome >= 90", "safari >= 14"],
|
|
19
|
+
polyfill: "usage" as const,
|
|
20
|
+
css: { target: "es2019" },
|
|
21
|
+
server: { external: ["sharp"] },
|
|
22
|
+
include: ["content"],
|
|
23
|
+
};
|
|
24
|
+
expect(buildManifest({ ...base, build }).build).toEqual(build);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("no build block, or an empty one, adds nothing", () => {
|
|
28
|
+
expect("build" in buildManifest(base)).toBe(false);
|
|
29
|
+
expect("build" in buildManifest({ ...base, build: {} })).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("invalid values throw with the field name", () => {
|
|
33
|
+
const bad: Array<[unknown, string]> = [
|
|
34
|
+
[{ target: [] }, "build.target"],
|
|
35
|
+
[{ target: [""] }, "build.target"],
|
|
36
|
+
[{ polyfill: "all" }, "build.polyfill"],
|
|
37
|
+
[{ polyfill: "usage" }, "polyfill needs build.target"],
|
|
38
|
+
[{ css: { target: 5 } }, "build.css.target"],
|
|
39
|
+
[{ sourcemap: "yes" }, "build.sourcemap"],
|
|
40
|
+
[{ server: { bundle: "no" } }, "build.server.bundle"],
|
|
41
|
+
[{ server: { external: [""] } }, "build.server.external"],
|
|
42
|
+
[{ include: "content" }, "build.include"],
|
|
43
|
+
];
|
|
44
|
+
for (const [build, msg] of bad) {
|
|
45
|
+
expect(() => buildManifest({ ...base, build: build as any })).toThrow(msg);
|
|
46
|
+
}
|
|
47
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -873,6 +873,207 @@ export interface AppManifest {
|
|
|
873
873
|
* `pylon deploy` refuses to ship when one is missing from the
|
|
874
874
|
* project's secrets. */
|
|
875
875
|
requiredEnv?: ManifestRequiredEnv[];
|
|
876
|
+
/** Production build settings (`pylon build`). */
|
|
877
|
+
build?: BuildConfig;
|
|
878
|
+
/** Realtime shard kinds whose simulation is a WebAssembly module. */
|
|
879
|
+
shards?: ManifestShard[];
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Production build settings for `pylon build`. Dev (`pylon dev`) ignores the
|
|
884
|
+
* browser settings; they apply to the artifact and to `pylon start` builds.
|
|
885
|
+
*
|
|
886
|
+
* ```ts
|
|
887
|
+
* buildManifest({
|
|
888
|
+
* // ...
|
|
889
|
+
* build: {
|
|
890
|
+
* target: ["chrome >= 90", "safari >= 14", "firefox >= 90"],
|
|
891
|
+
* polyfill: "usage",
|
|
892
|
+
* server: { external: ["sharp"] },
|
|
893
|
+
* },
|
|
894
|
+
* });
|
|
895
|
+
* ```
|
|
896
|
+
*/
|
|
897
|
+
export interface BuildConfig {
|
|
898
|
+
/** Browser target for client JavaScript: an ECMAScript edition
|
|
899
|
+
* (`"es2018"` to `"es2022"`) or browserslist queries. Unset: modern
|
|
900
|
+
* output, no lowering. Needs `@swc/core` and `browserslist` in the app's
|
|
901
|
+
* devDependencies. The client runtime needs ES modules and dynamic
|
|
902
|
+
* `import()`, so browsers older than Chrome 63, Firefox 67, and Safari
|
|
903
|
+
* 11.1 cannot run it at any target. */
|
|
904
|
+
target?: string | string[];
|
|
905
|
+
/** core-js polyfills for `target`. `"usage"` adds the features the bundle
|
|
906
|
+
* uses; `"entry"` adds every feature the target lacks. Needs `core-js` in
|
|
907
|
+
* the app's dependencies. Default `false`. */
|
|
908
|
+
polyfill?: false | "usage" | "entry";
|
|
909
|
+
css?: {
|
|
910
|
+
/** CSS target (prefixes, nesting, color syntax), same forms as
|
|
911
|
+
* `target`. Default: `target`. Needs `lightningcss` and `browserslist`. */
|
|
912
|
+
target?: string | string[];
|
|
913
|
+
};
|
|
914
|
+
/** Emit source maps for the client and server bundles. Default `false`. */
|
|
915
|
+
sourcemap?: boolean;
|
|
916
|
+
server?: {
|
|
917
|
+
/** Bundle npm packages into the server output. `false` ships every
|
|
918
|
+
* dependency in `server/node_modules`. Default `true`. */
|
|
919
|
+
bundle?: boolean;
|
|
920
|
+
/** Packages to leave out of the server bundle and ship in
|
|
921
|
+
* `server/node_modules` with their dependencies: native modules and
|
|
922
|
+
* packages that read their own files at run time. */
|
|
923
|
+
external?: string[];
|
|
924
|
+
};
|
|
925
|
+
/** Project files or directories to copy into the artifact, for files the
|
|
926
|
+
* app reads from disk at run time (content/, data/). `public/` and image
|
|
927
|
+
* files under `app/` are always copied. */
|
|
928
|
+
include?: string[];
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/** Throws when `build` has a value `pylon build` cannot use. */
|
|
932
|
+
function validateBuildConfig(build: BuildConfig): void {
|
|
933
|
+
const fail = (msg: string): never => {
|
|
934
|
+
throw new Error(`buildManifest: build.${msg}`);
|
|
935
|
+
};
|
|
936
|
+
const checkTarget = (t: unknown, field: string) => {
|
|
937
|
+
if (t === undefined) return;
|
|
938
|
+
const list = Array.isArray(t) ? t : [t];
|
|
939
|
+
if (list.length === 0 || list.some((x) => typeof x !== "string" || x.trim() === "")) {
|
|
940
|
+
fail(`${field} must be a non-empty string or a list of non-empty strings`);
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
checkTarget(build.target, "target");
|
|
944
|
+
checkTarget(build.css?.target, "css.target");
|
|
945
|
+
const pf = build.polyfill;
|
|
946
|
+
if (pf !== undefined && pf !== false && pf !== "usage" && pf !== "entry") {
|
|
947
|
+
fail(`polyfill must be false, "usage", or "entry"`);
|
|
948
|
+
}
|
|
949
|
+
if (pf && build.target === undefined) {
|
|
950
|
+
fail(`polyfill needs build.target (the browsers to polyfill for)`);
|
|
951
|
+
}
|
|
952
|
+
if (build.sourcemap !== undefined && typeof build.sourcemap !== "boolean") {
|
|
953
|
+
fail(`sourcemap must be a boolean`);
|
|
954
|
+
}
|
|
955
|
+
if (build.server?.bundle !== undefined && typeof build.server.bundle !== "boolean") {
|
|
956
|
+
fail(`server.bundle must be a boolean`);
|
|
957
|
+
}
|
|
958
|
+
for (const [field, list] of [
|
|
959
|
+
["server.external", build.server?.external],
|
|
960
|
+
["include", build.include],
|
|
961
|
+
] as const) {
|
|
962
|
+
if (list === undefined) continue;
|
|
963
|
+
if (!Array.isArray(list) || list.some((x) => typeof x !== "string" || x === "")) {
|
|
964
|
+
fail(`${field} must be a list of non-empty strings`);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** One realtime shard kind. See {@link shard}. */
|
|
970
|
+
export interface ManifestShard {
|
|
971
|
+
/** The kind name `ctx.shards.create(kind, id)` takes. */
|
|
972
|
+
name: string;
|
|
973
|
+
/** The compiled module, relative to the app root. */
|
|
974
|
+
wasm: string;
|
|
975
|
+
/** A Cargo crate, relative to the app root, that `pylon shards build`
|
|
976
|
+
* compiles to `wasm`. `pylon dev` runs that build at start. */
|
|
977
|
+
crate?: string;
|
|
978
|
+
/** Snapshot and input codec. Default `"json"`. */
|
|
979
|
+
codec?: "json" | "msgpack";
|
|
980
|
+
/** Ticks per second. `0` ticks only when inputs arrive. Default 20. */
|
|
981
|
+
tickRate?: number;
|
|
982
|
+
/** Pass `tick` a constant `1 / tickRate` instead of the measured time,
|
|
983
|
+
* so a replay gives the same result. Default `true`. */
|
|
984
|
+
fixedTimestep?: boolean;
|
|
985
|
+
/** Subscribers one shard admits. Default 256. */
|
|
986
|
+
maxSubscribers?: number;
|
|
987
|
+
/** Shards of this kind that may run at once. Default 64. */
|
|
988
|
+
maxInstances?: number;
|
|
989
|
+
/** Memory cap per shard, in MiB. Default 64. */
|
|
990
|
+
memoryMb?: number;
|
|
991
|
+
/** Time budget for one tick (its inputs, `tick`, and the snapshots) or
|
|
992
|
+
* one authorize call, in milliseconds. A tick that runs longer stops the
|
|
993
|
+
* shard. Default 100. */
|
|
994
|
+
tickBudgetMs?: number;
|
|
995
|
+
/** Stop a shard after this many seconds with no subscribers and no
|
|
996
|
+
* inputs. `0` never stops it. Default 90. */
|
|
997
|
+
idleShutdownSecs?: number;
|
|
998
|
+
/** Per-subscriber input limits. */
|
|
999
|
+
input?: {
|
|
1000
|
+
/** Sustained inputs per second. Default 120. */
|
|
1001
|
+
ratePerSec?: number;
|
|
1002
|
+
/** Inputs above the rate a subscriber may send in a burst. Default 240. */
|
|
1003
|
+
burst?: number;
|
|
1004
|
+
/** Inputs one subscriber may have queued. Default 256. */
|
|
1005
|
+
maxQueued?: number;
|
|
1006
|
+
/** Inputs applied per subscriber per tick. Default 32. */
|
|
1007
|
+
maxPerTick?: number;
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Declare a realtime shard kind whose simulation is a WebAssembly module.
|
|
1013
|
+
* Build the module with the `pylon-shard-guest` Rust crate (or any
|
|
1014
|
+
* toolchain that implements its ABI). The stock `pylon` binary runs it, on
|
|
1015
|
+
* Pylon Cloud too.
|
|
1016
|
+
*
|
|
1017
|
+
* ```ts
|
|
1018
|
+
* buildManifest({
|
|
1019
|
+
* // ...
|
|
1020
|
+
* shards: [
|
|
1021
|
+
* shard({ name: "arena", wasm: "shards/arena.wasm", crate: "shards/arena", tickRate: 30 }),
|
|
1022
|
+
* ],
|
|
1023
|
+
* });
|
|
1024
|
+
* ```
|
|
1025
|
+
*
|
|
1026
|
+
* A function starts a shard with `ctx.shards.create("arena", matchId, params)`
|
|
1027
|
+
* and mints tickets with `ctx.shards.ticket(matchId)`. Clients connect with
|
|
1028
|
+
* `useShard(matchId, { ticket })`.
|
|
1029
|
+
*/
|
|
1030
|
+
export function shard(def: ManifestShard): ManifestShard {
|
|
1031
|
+
validateShard(def);
|
|
1032
|
+
return def;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const SHARD_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
1036
|
+
|
|
1037
|
+
/** Throws when a shard kind has a value the runtime cannot use. */
|
|
1038
|
+
function validateShard(def: ManifestShard): void {
|
|
1039
|
+
const fail = (msg: string): never => {
|
|
1040
|
+
throw new Error(`shard(${JSON.stringify(def?.name ?? "")}): ${msg}`);
|
|
1041
|
+
};
|
|
1042
|
+
if (typeof def?.name !== "string" || !SHARD_NAME.test(def.name)) {
|
|
1043
|
+
fail("name must start with a letter and hold up to 64 letters, digits, '_', '-'");
|
|
1044
|
+
}
|
|
1045
|
+
const checkPath = (value: unknown, field: string) => {
|
|
1046
|
+
if (typeof value !== "string" || value === "") fail(`${field} must be a non-empty path`);
|
|
1047
|
+
const p = (value as string).replace(/\\/g, "/");
|
|
1048
|
+
if (p.startsWith("/") || /^[A-Za-z]:/.test(p) || p.split("/").includes("..")) {
|
|
1049
|
+
fail(`${field} must be relative to the app root, without ".."`);
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
checkPath(def.wasm, "wasm");
|
|
1053
|
+
if (!def.wasm.endsWith(".wasm")) fail(`wasm must name a .wasm file`);
|
|
1054
|
+
if (def.crate !== undefined) checkPath(def.crate, "crate");
|
|
1055
|
+
if (def.codec !== undefined && def.codec !== "json" && def.codec !== "msgpack") {
|
|
1056
|
+
fail(`codec must be "json" or "msgpack"`);
|
|
1057
|
+
}
|
|
1058
|
+
if (def.fixedTimestep !== undefined && typeof def.fixedTimestep !== "boolean") {
|
|
1059
|
+
fail("fixedTimestep must be a boolean");
|
|
1060
|
+
}
|
|
1061
|
+
const checkInt = (value: unknown, field: string, min: number, max: number) => {
|
|
1062
|
+
if (value === undefined) return;
|
|
1063
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
|
|
1064
|
+
fail(`${field} must be an integer from ${min} to ${max}`);
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
checkInt(def.tickRate, "tickRate", 0, 1000);
|
|
1068
|
+
checkInt(def.maxSubscribers, "maxSubscribers", 1, 1_000_000);
|
|
1069
|
+
checkInt(def.maxInstances, "maxInstances", 1, 100_000);
|
|
1070
|
+
checkInt(def.memoryMb, "memoryMb", 1, 4096);
|
|
1071
|
+
checkInt(def.tickBudgetMs, "tickBudgetMs", 1, 60_000);
|
|
1072
|
+
checkInt(def.idleShutdownSecs, "idleShutdownSecs", 0, 31_536_000);
|
|
1073
|
+
checkInt(def.input?.ratePerSec, "input.ratePerSec", 1, 100_000);
|
|
1074
|
+
checkInt(def.input?.burst, "input.burst", 1, 100_000);
|
|
1075
|
+
checkInt(def.input?.maxQueued, "input.maxQueued", 1, 100_000);
|
|
1076
|
+
checkInt(def.input?.maxPerTick, "input.maxPerTick", 1, 100_000);
|
|
876
1077
|
}
|
|
877
1078
|
|
|
878
1079
|
/** One environment variable the app declares it needs. */
|
|
@@ -2150,6 +2351,10 @@ export function buildManifest(options: {
|
|
|
2150
2351
|
* ```
|
|
2151
2352
|
*/
|
|
2152
2353
|
requiredEnv?: ManifestRequiredEnv[];
|
|
2354
|
+
/** Production build settings for `pylon build`. See `BuildConfig`. */
|
|
2355
|
+
build?: BuildConfig;
|
|
2356
|
+
/** Realtime shard kinds. See `shard`. */
|
|
2357
|
+
shards?: ManifestShard[];
|
|
2153
2358
|
/** Set by `discoverFunctions()` (spread its result into this call).
|
|
2154
2359
|
* When true, the framework's AgentRun/AgentMessage entities and
|
|
2155
2360
|
* their owner-scoping policies are appended to the manifest —
|
|
@@ -2170,6 +2375,15 @@ export function buildManifest(options: {
|
|
|
2170
2375
|
// can `.policies(policy({ allowRead: "..." }))` without one. Auto-
|
|
2171
2376
|
// derive from the entity + a counter so two attached policies
|
|
2172
2377
|
// don't collide.
|
|
2378
|
+
if (options.build) validateBuildConfig(options.build);
|
|
2379
|
+
const shardNames = new Set<string>();
|
|
2380
|
+
for (const def of options.shards ?? []) {
|
|
2381
|
+
validateShard(def);
|
|
2382
|
+
if (shardNames.has(def.name)) {
|
|
2383
|
+
throw new Error(`buildManifest: two shard kinds are named "${def.name}"`);
|
|
2384
|
+
}
|
|
2385
|
+
shardNames.add(def.name);
|
|
2386
|
+
}
|
|
2173
2387
|
const attached: PolicyDefinition[] = [];
|
|
2174
2388
|
for (const ent of options.entities) {
|
|
2175
2389
|
const extracted = extractAttachedPolicies(ent);
|
|
@@ -2233,6 +2447,12 @@ export function buildManifest(options: {
|
|
|
2233
2447
|
...(options.requiredEnv && options.requiredEnv.length > 0
|
|
2234
2448
|
? { requiredEnv: options.requiredEnv }
|
|
2235
2449
|
: {}),
|
|
2450
|
+
...(options.build && Object.keys(options.build).length > 0
|
|
2451
|
+
? { build: options.build }
|
|
2452
|
+
: {}),
|
|
2453
|
+
...(options.shards && options.shards.length > 0
|
|
2454
|
+
? { shards: options.shards }
|
|
2455
|
+
: {}),
|
|
2236
2456
|
};
|
|
2237
2457
|
}
|
|
2238
2458
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `shard({...})` and `buildManifest({ shards })`: shard kinds reach the
|
|
3
|
+
* manifest as written, and a value the runtime cannot use fails when app.ts
|
|
4
|
+
* runs.
|
|
5
|
+
*/
|
|
6
|
+
import { expect, test } from "bun:test";
|
|
7
|
+
import { buildManifest, entity, field, shard } from "./index";
|
|
8
|
+
|
|
9
|
+
const base = {
|
|
10
|
+
name: "t",
|
|
11
|
+
version: "0",
|
|
12
|
+
entities: [entity("Doc", { title: field.string() })],
|
|
13
|
+
routes: [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
test("shard kinds are copied into the manifest", () => {
|
|
17
|
+
const arena = shard({
|
|
18
|
+
name: "arena",
|
|
19
|
+
wasm: "shards/arena.wasm",
|
|
20
|
+
crate: "shards/arena",
|
|
21
|
+
codec: "msgpack",
|
|
22
|
+
tickRate: 30,
|
|
23
|
+
maxInstances: 8,
|
|
24
|
+
input: { ratePerSec: 60 },
|
|
25
|
+
});
|
|
26
|
+
expect(buildManifest({ ...base, shards: [arena] }).shards).toEqual([arena]);
|
|
27
|
+
expect("shards" in buildManifest(base)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("bad shard kinds throw", () => {
|
|
31
|
+
expect(() => shard({ name: "1bad", wasm: "a.wasm" })).toThrow("name must start");
|
|
32
|
+
expect(() => shard({ name: "a", wasm: "../a.wasm" })).toThrow("relative to the app root");
|
|
33
|
+
expect(() => shard({ name: "a", wasm: "/abs/a.wasm" })).toThrow("relative to the app root");
|
|
34
|
+
expect(() => shard({ name: "a", wasm: "a.js" })).toThrow(".wasm file");
|
|
35
|
+
expect(() => shard({ name: "a", wasm: "a.wasm", codec: "bincode" as never })).toThrow("codec");
|
|
36
|
+
expect(() => shard({ name: "a", wasm: "a.wasm", tickRate: 1.5 })).toThrow("tickRate");
|
|
37
|
+
expect(() => shard({ name: "a", wasm: "a.wasm", memoryMb: 0 })).toThrow("memoryMb");
|
|
38
|
+
const a = { name: "a", wasm: "a.wasm" };
|
|
39
|
+
expect(() => buildManifest({ ...base, shards: [a, a] })).toThrow('two shard kinds are named "a"');
|
|
40
|
+
});
|