@voxgig/sdkgen 3.4.6 → 3.5.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 (198) hide show
  1. package/bin/voxgig-sdkgen +78 -10
  2. package/dist/action/action.d.ts +5 -2
  3. package/dist/action/action.js +113 -6
  4. package/dist/action/action.js.map +1 -1
  5. package/dist/action/check.d.ts +15 -0
  6. package/dist/action/check.js +347 -0
  7. package/dist/action/check.js.map +1 -0
  8. package/dist/action/dispatch.d.ts +8 -0
  9. package/dist/action/dispatch.js +74 -0
  10. package/dist/action/dispatch.js.map +1 -0
  11. package/dist/action/docs.d.ts +4 -0
  12. package/dist/action/docs.js +236 -0
  13. package/dist/action/docs.js.map +1 -0
  14. package/dist/action/doctor.d.ts +5 -2
  15. package/dist/action/doctor.js +526 -74
  16. package/dist/action/doctor.js.map +1 -1
  17. package/dist/action/feature.js +96 -17
  18. package/dist/action/feature.js.map +1 -1
  19. package/dist/action/kind.d.ts +30 -0
  20. package/dist/action/kind.js +258 -0
  21. package/dist/action/kind.js.map +1 -0
  22. package/dist/action/package.d.ts +25 -0
  23. package/dist/action/package.js +852 -0
  24. package/dist/action/package.js.map +1 -0
  25. package/dist/action/resolve.d.ts +18 -0
  26. package/dist/action/resolve.js +280 -0
  27. package/dist/action/resolve.js.map +1 -0
  28. package/dist/action/target.d.ts +7 -1
  29. package/dist/action/target.js +240 -97
  30. package/dist/action/target.js.map +1 -1
  31. package/dist/cmp/Docs.d.ts +4 -0
  32. package/dist/cmp/Docs.js +98 -0
  33. package/dist/cmp/Docs.js.map +1 -0
  34. package/dist/cmp/ExternalDocs.d.ts +2 -0
  35. package/dist/cmp/ExternalDocs.js +43 -0
  36. package/dist/cmp/ExternalDocs.js.map +1 -0
  37. package/dist/cmp/ReadmeTop.js +5 -1
  38. package/dist/cmp/ReadmeTop.js.map +1 -1
  39. package/dist/helpers/definition.d.ts +5 -0
  40. package/dist/helpers/definition.js +72 -0
  41. package/dist/helpers/definition.js.map +1 -0
  42. package/dist/helpers/featureSource.d.ts +8 -2
  43. package/dist/helpers/featureSource.js +61 -12
  44. package/dist/helpers/featureSource.js.map +1 -1
  45. package/dist/helpers/junk.d.ts +11 -0
  46. package/dist/helpers/junk.js +149 -0
  47. package/dist/helpers/junk.js.map +1 -0
  48. package/dist/helpers/manifest.d.ts +50 -0
  49. package/dist/helpers/manifest.js +328 -0
  50. package/dist/helpers/manifest.js.map +1 -0
  51. package/dist/helpers/modelcheck.d.ts +22 -0
  52. package/dist/helpers/modelcheck.js +203 -0
  53. package/dist/helpers/modelcheck.js.map +1 -0
  54. package/dist/helpers/semver.d.ts +4 -0
  55. package/dist/helpers/semver.js +174 -0
  56. package/dist/helpers/semver.js.map +1 -0
  57. package/dist/helpers/shipped.d.ts +4 -0
  58. package/dist/helpers/shipped.js +36 -0
  59. package/dist/helpers/shipped.js.map +1 -0
  60. package/dist/helpers/stdrep.d.ts +9 -1
  61. package/dist/helpers/stdrep.js +22 -0
  62. package/dist/helpers/stdrep.js.map +1 -1
  63. package/dist/sdkgen.d.ts +15 -3
  64. package/dist/sdkgen.js +233 -67
  65. package/dist/sdkgen.js.map +1 -1
  66. package/dist/tsconfig.tsbuildinfo +1 -1
  67. package/dist/types.d.ts +10 -2
  68. package/dist/utility.js +17 -0
  69. package/dist/utility.js.map +1 -1
  70. package/model/sdkgen.aontu +69 -0
  71. package/package.json +3 -2
  72. package/project/.sdk/model/feature/audit.aontu +1 -0
  73. package/project/.sdk/model/feature/cache.aontu +1 -0
  74. package/project/.sdk/model/feature/clienttrack.aontu +1 -0
  75. package/project/.sdk/model/feature/debug.aontu +1 -0
  76. package/project/.sdk/model/feature/idempotency.aontu +1 -0
  77. package/project/.sdk/model/feature/log.aontu +1 -0
  78. package/project/.sdk/model/feature/metrics.aontu +1 -0
  79. package/project/.sdk/model/feature/netsim.aontu +1 -0
  80. package/project/.sdk/model/feature/paging.aontu +1 -0
  81. package/project/.sdk/model/feature/proxy.aontu +1 -0
  82. package/project/.sdk/model/feature/ratelimit.aontu +1 -0
  83. package/project/.sdk/model/feature/rbac.aontu +1 -0
  84. package/project/.sdk/model/feature/retry.aontu +1 -0
  85. package/project/.sdk/model/feature/streaming.aontu +1 -0
  86. package/project/.sdk/model/feature/telemetry.aontu +1 -0
  87. package/project/.sdk/model/feature/test.aontu +1 -0
  88. package/project/.sdk/model/feature/timeout.aontu +1 -0
  89. package/project/.sdk/model/target/c.aontu +1 -0
  90. package/project/.sdk/model/target/clojure.aontu +1 -0
  91. package/project/.sdk/model/target/cpp.aontu +1 -0
  92. package/project/.sdk/model/target/dart.aontu +1 -0
  93. package/project/.sdk/model/target/elixir.aontu +1 -0
  94. package/project/.sdk/model/target/go-cli.aontu +1 -0
  95. package/project/.sdk/model/target/go-mcp.aontu +1 -0
  96. package/project/.sdk/model/target/go.aontu +1 -0
  97. package/project/.sdk/model/target/haskell.aontu +1 -0
  98. package/project/.sdk/model/target/java.aontu +1 -0
  99. package/project/.sdk/model/target/js.aontu +1 -0
  100. package/project/.sdk/model/target/kotlin.aontu +1 -0
  101. package/project/.sdk/model/target/lean.aontu +1 -0
  102. package/project/.sdk/model/target/lua.aontu +1 -0
  103. package/project/.sdk/model/target/ocaml.aontu +1 -0
  104. package/project/.sdk/model/target/perl.aontu +1 -0
  105. package/project/.sdk/model/target/php.aontu +1 -0
  106. package/project/.sdk/model/target/py-data.aontu +1 -0
  107. package/project/.sdk/model/target/py.aontu +1 -0
  108. package/project/.sdk/model/target/rb.aontu +1 -0
  109. package/project/.sdk/model/target/rust.aontu +1 -0
  110. package/project/.sdk/model/target/scala.aontu +1 -0
  111. package/project/.sdk/model/target/seneca-provider.aontu +1 -0
  112. package/project/.sdk/model/target/zig.aontu +1 -0
  113. package/project/.sdk/src/cmp/c/Config_c.ts +44 -0
  114. package/project/.sdk/src/cmp/c/fragment/Main.fragment.c +3 -1
  115. package/project/.sdk/src/cmp/clojure/Config_clojure.ts +37 -42
  116. package/project/.sdk/src/cmp/clojure/utility_clojure.ts +42 -0
  117. package/project/.sdk/src/cmp/cpp/Config_cpp.ts +18 -0
  118. package/project/.sdk/src/cmp/csharp/Config_csharp.ts +125 -52
  119. package/project/.sdk/src/cmp/csharp/fragment/Main.fragment.cs +3 -1
  120. package/project/.sdk/src/cmp/csharp/utility_csharp.ts +94 -6
  121. package/project/.sdk/src/cmp/dart/Config_dart.ts +56 -3
  122. package/project/.sdk/src/cmp/dart/fragment/Config.data.fragment.dart +74 -0
  123. package/project/.sdk/src/cmp/dart/fragment/Config.fragment.dart +6 -9
  124. package/project/.sdk/src/cmp/dart/fragment/Main.fragment.dart +16 -5
  125. package/project/.sdk/src/cmp/dart/utility_dart.ts +18 -0
  126. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +128 -13
  127. package/project/.sdk/src/cmp/elixir/fragment/Main.fragment.ex +3 -1
  128. package/project/.sdk/src/cmp/elixir/utility_elixir.ts +6 -0
  129. package/project/.sdk/src/cmp/haskell/Config_haskell.ts +35 -52
  130. package/project/.sdk/src/cmp/haskell/Package_haskell.ts +1 -0
  131. package/project/.sdk/src/cmp/haskell/Test_haskell.ts +1 -1
  132. package/project/.sdk/src/cmp/haskell/utility_haskell.ts +15 -5
  133. package/project/.sdk/src/cmp/java/Config_java.ts +21 -0
  134. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +15 -0
  135. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +42 -39
  136. package/project/.sdk/src/cmp/perl/Config_perl.ts +15 -0
  137. package/project/.sdk/src/cmp/perl/fragment/Main.fragment.pm +3 -1
  138. package/project/.sdk/src/cmp/rust/Config_rust.ts +44 -0
  139. package/project/.sdk/src/cmp/rust/fragment/Main.fragment.rs +4 -2
  140. package/project/.sdk/src/cmp/scala/Config_scala.ts +15 -0
  141. package/project/.sdk/src/cmp/swift/Config_swift.ts +17 -0
  142. package/project/.sdk/src/cmp/swift/fragment/Main.fragment.swift +3 -1
  143. package/project/.sdk/src/cmp/zig/Config_zig.ts +44 -0
  144. package/project/.sdk/src/cmp/zig/TestEntity_zig.ts +9 -2
  145. package/project/.sdk/src/cmp/zig/fragment/Main.fragment.zig +31 -18
  146. package/project/.sdk/tm/c/core/sdk.h +1 -0
  147. package/project/.sdk/tm/c/utility/make_options.c +5 -1
  148. package/project/.sdk/tm/clojure/src/sdk/core.clj +51 -3
  149. package/project/.sdk/tm/cpp/core/types.hpp +4 -1
  150. package/project/.sdk/tm/cpp/utility/pipeline.hpp +5 -1
  151. package/project/.sdk/tm/csharp/test/PipelineTest.cs +15 -0
  152. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +67 -1
  153. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +43 -0
  154. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +58 -2
  155. package/project/.sdk/tm/haskell/{test/TestJson.hs → src/SdkJson.hs} +8 -4
  156. package/project/.sdk/tm/haskell/src/SdkRuntime.hs +55 -2
  157. package/project/.sdk/tm/haskell/test/Runner.hs +1 -1
  158. package/project/.sdk/tm/haskell/test/StructCorpus.hs +1 -1
  159. package/project/.sdk/tm/java/core/SdkClient.java +3 -1
  160. package/project/.sdk/tm/java/utility/MakeOptions.java +5 -1
  161. package/project/.sdk/tm/kotlin/core/SdkClient.kt +3 -1
  162. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +5 -1
  163. package/project/.sdk/tm/ocaml/Makefile +3 -2
  164. package/project/.sdk/tm/ocaml/sdk_json.ml +106 -0
  165. package/project/.sdk/tm/ocaml/sdk_runtime.ml +56 -0
  166. package/project/.sdk/tm/ocaml/test/struct_corpus.ml +4 -86
  167. package/project/.sdk/tm/perl/utility/make_options.pm +5 -1
  168. package/project/.sdk/tm/rust/utility/make_options.rs +79 -1
  169. package/project/.sdk/tm/scala/core/SdkClient.scala +3 -1
  170. package/project/.sdk/tm/scala/utility/Make.scala +5 -1
  171. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +5 -1
  172. package/project/.sdk/tm/zig/core/utility.zig +85 -1
  173. package/project/.sdk/tm/zig/test/gotcha_test.zig +8 -8
  174. package/project/sdkgen-package.json +57 -0
  175. package/src/action/action.ts +136 -6
  176. package/src/action/check.ts +495 -0
  177. package/src/action/dispatch.ts +105 -0
  178. package/src/action/docs.ts +310 -0
  179. package/src/action/doctor.ts +692 -81
  180. package/src/action/feature.ts +109 -17
  181. package/src/action/kind.ts +375 -0
  182. package/src/action/package.ts +1173 -0
  183. package/src/action/resolve.ts +362 -0
  184. package/src/action/target.ts +290 -108
  185. package/src/cmp/Docs.ts +125 -0
  186. package/src/cmp/ExternalDocs.ts +54 -0
  187. package/src/cmp/ReadmeTop.ts +6 -1
  188. package/src/helpers/definition.ts +82 -0
  189. package/src/helpers/featureSource.ts +83 -13
  190. package/src/helpers/junk.ts +168 -0
  191. package/src/helpers/manifest.ts +474 -0
  192. package/src/helpers/modelcheck.ts +262 -0
  193. package/src/helpers/semver.ts +211 -0
  194. package/src/helpers/shipped.ts +41 -0
  195. package/src/helpers/stdrep.ts +70 -0
  196. package/src/sdkgen.ts +294 -66
  197. package/src/types.ts +33 -3
  198. package/src/utility.ts +17 -0
@@ -0,0 +1,74 @@
1
+ import 'dart:convert';
2
+
3
+ import 'feature/base/BaseFeature.dart';
4
+ // #ImportFeatures
5
+
6
+ // ignore: non_constant_identifier_names
7
+ final Map<String, BaseFeature Function()> FEATURE_CLASS = {
8
+ // #FeatureClasses
9
+ };
10
+
11
+ // THE API MODEL, EMBEDDED AS DATA (sdkgen rung L1).
12
+ //
13
+ // The literal form of this file declares the whole model as nested map
14
+ // literals. For a large API that is megabytes of source the Dart analyzer and
15
+ // compiler must parse and type every node of, on every build, and that the VM
16
+ // must build entry by entry on every load.
17
+ //
18
+ // As a single string constant it is one token, and `jsonDecode` builds the map
19
+ // far faster than the equivalent literal.
20
+ //
21
+ // Emitted only above a size threshold, or when `main.kit.config.repr` pins it:
22
+ // for a small model the literal is smaller, loads no slower, and is far easier
23
+ // to read when debugging.
24
+ const String _CONFIG_DATA = 'CONFIGJSON';
25
+
26
+ class Config {
27
+ // ONE decode per constructed Config, and each instance owns the maps it
28
+ // decoded. The literal representation builds fresh maps in every field
29
+ // initialiser, so a caller that constructs a second Config and mutates it
30
+ // does not touch the first - and `Config` is exported by Main.fragment.dart,
31
+ // so callers really can construct one.
32
+ //
33
+ // Sharing a single top-level parsed map would have been cheaper and was the
34
+ // first attempt; it made `Config().options['x'] = 1` mutate the `config`
35
+ // singleton every SDK client reads. Decoding per instance costs about what
36
+ // building the literal per instance cost, and the rung's win is untouched:
37
+ // L1 is about what the COMPILER has to walk, not what happens at load.
38
+ Config() : this._(jsonDecode(_CONFIG_DATA) as Map<String, dynamic>);
39
+
40
+ Config._(Map<String, dynamic> data)
41
+ : main = data['main'] as Map<String, dynamic>,
42
+ feature = data['feature'] as Map<String, dynamic>,
43
+ options = data['options'] as Map<String, dynamic>,
44
+ entity = data['entity'] as Map<String, dynamic>;
45
+
46
+ BaseFeature makeFeature(String fn) {
47
+ final fc = FEATURE_CLASS[fn];
48
+ if (null == fc) {
49
+ // TODO: errors etc
50
+ throw StateError('Unknown feature: ' + fn);
51
+ }
52
+ return fc();
53
+ }
54
+
55
+ // The same fields the literal declares - same names, same types, same
56
+ // finalness - so callers cannot tell which representation they were given.
57
+ final Map<String, dynamic> main;
58
+
59
+ final Map<String, dynamic> feature;
60
+
61
+ final Map<String, dynamic> options;
62
+
63
+ final Map<String, dynamic> entity;
64
+
65
+ // The pipeline context carries the config as a plain map.
66
+ Map<String, dynamic> toMap() => <String, dynamic>{
67
+ 'main': main,
68
+ 'feature': feature,
69
+ 'options': options,
70
+ 'entity': entity,
71
+ };
72
+ }
73
+
74
+ final config = Config();
@@ -24,15 +24,12 @@ class Config {
24
24
  // #FeatureConfigs
25
25
  };
26
26
 
27
- final Map<String, dynamic> options = <String, dynamic>{
28
- 'base': '$$main.kit.info.servers.0.url$$',
29
-
30
- 'AUTHBLOCK''headers': 'HEADERS',
31
-
32
- 'entity': <String, dynamic>{
33
- // #EntityConfigs
34
- }
35
- };
27
+ // Rendered whole from the canonical config definition rather than assembled
28
+ // slot by slot. Assembling it here meant `options.server` - the OpenAPI
29
+ // server-variable defaults - was simply absent from this branch, so a
30
+ // templated server URL produced a different config either side of the
31
+ // threshold.
32
+ final Map<String, dynamic> options = 'OPTIONSMAP';
36
33
 
37
34
  final Map<String, dynamic> entity = 'ENTITYMAP';
38
35
 
@@ -9,20 +9,31 @@ import 'Spec.dart';
9
9
  // a Dart `export` needs no matching `import`, so importing them here too is an
10
10
  // unused_import. Keep only the imports actually referenced in this file.
11
11
  import 'utility/ErrUtility.dart';
12
- import 'utility/Utility.dart';
12
+ // PREFIXED, and deliberately not re-exported. The runtime helper class is
13
+ // named `Utility`, and so is the generated data class for an entity named
14
+ // `utility` (ProjectNameTypes.dart) — exporting both from this library makes
15
+ // the name ambiguous and NO dart SDK for such an API compiles:
16
+ //
17
+ // Error: 'Utility' is exported from both 'lib/ProjectNameTypes.dart'
18
+ // and 'lib/utility/Utility.dart'
19
+ //
20
+ // The entity type is the user-facing name, so it keeps the plain one; the
21
+ // runtime helper is internal plumbing and is reached through the prefix. Any
22
+ // entity name can collide with an internal class, so the prefix — not a
23
+ // rename — is what makes this safe for every API.
24
+ import 'utility/Utility.dart' as sdkutil;
13
25
 
14
26
  export 'Config.dart' show Config, config;
15
27
  export 'ProjectNameEntityBase.dart' show ProjectNameEntityBase;
16
28
  export 'ProjectNameError.dart' show ProjectNameError;
17
29
  export 'feature/base/BaseFeature.dart' show BaseFeature;
18
- export 'utility/Utility.dart' show Utility;
19
30
 
20
- final Utility stdutil = Utility();
31
+ final sdkutil.Utility stdutil = sdkutil.Utility();
21
32
 
22
33
  class ProjectNameSDK {
23
34
  String mode = 'live';
24
35
  dynamic _options;
25
- final Utility _utility = Utility();
36
+ final sdkutil.Utility _utility = sdkutil.Utility();
26
37
  List<dynamic> features = [];
27
38
  dynamic rootctx;
28
39
 
@@ -85,7 +96,7 @@ class ProjectNameSDK {
85
96
  return _utility.struct.clone(_options);
86
97
  }
87
98
 
88
- Utility utility() {
99
+ sdkutil.Utility utility() {
89
100
  return _utility;
90
101
  }
91
102
 
@@ -129,7 +129,25 @@ function clean(o: any, dropDefaults?: boolean): any {
129
129
  }
130
130
 
131
131
 
132
+
133
+ // The JSON as a Dart string literal.
134
+ //
135
+ // Dart's ordinary string literals INTERPOLATE: `$name` and `${...}` are
136
+ // substitution, and the model is full of `$` - `$STRING`, `$NUMBER`,
137
+ // `$action`. A raw string (`r'...'`) would avoid that but cannot contain its
138
+ // own quote character and has no escape for it, and the JSON contains both
139
+ // quote characters in quantity.
140
+ //
141
+ // So: JSON.stringify's escaping, which already handles `"` and `\` and emits
142
+ // no raw control characters, plus `$` -> `\$`. That last step is unambiguous
143
+ // precisely because JSON.stringify has already escaped every backslash, so a
144
+ // `\$` here can only have come from this rule.
145
+ function dartStringLiteral(json: string): string {
146
+ return JSON.stringify(json).replace(/\$/g, '\\$')
147
+ }
148
+
132
149
  export {
150
+ dartStringLiteral,
133
151
  clean,
134
152
  dartString,
135
153
  dartValue,
@@ -4,8 +4,11 @@ import {
4
4
  File,
5
5
  Folder,
6
6
  cmp,
7
+ configDefinition,
8
+ configReprSetting,
7
9
  each,
8
10
  isAuthActive,
11
+ isConfigData,
9
12
  resolveAuthPrefix,
10
13
  } from '@voxgig/sdkgen'
11
14
 
@@ -53,8 +56,101 @@ const Config = cmp(async function Config(props: any) {
53
56
  }, true), a), {})
54
57
 
55
58
  Folder({ name: 'lib' }, () => {
59
+ // The same config as an OBJECT, built by the shared helper so this
60
+ // target's literal and the data that replaces it above the threshold are
61
+ // the same config by construction. The JSON is what the threshold is
62
+ // measured on - emitted source size varies by language, the model does not.
63
+ const { def: configDef, json: configJson } = configDefinition(model)
64
+ const asData = isConfigData(configJson, configReprSetting(model))
65
+
56
66
  File({ name: 'config.ex' }, () => {
57
67
 
68
+ // ABOVE THE THRESHOLD: emit the model as DATA.
69
+ //
70
+ // The literal is one nested `%{}` the Elixir compiler expands and holds
71
+ // in the module's constant pool; a binary is one token. `Json.parse`
72
+ // builds the vendored struct's heap nodes DIRECTLY - the same nodes
73
+ // `Helpers.deep/1` produces from a plain map - so make_config returns
74
+ // exactly what it returned before.
75
+ //
76
+ // `ProjectName.Json` is already the SDK's response decoder (see
77
+ // `safe_json` in utility.ex), so this adds no dependency.
78
+ if (asData) {
79
+ Content(`# ${Name} SDK configuration
80
+ #
81
+ # THE API MODEL, EMBEDDED AS DATA (sdkgen rung L1).
82
+ #
83
+ # Emitted only above a size threshold, or when \`main.kit.config.repr\` pins
84
+ # it: for a small model the literal is smaller and far easier to read when
85
+ # debugging. Do not edit by hand.
86
+
87
+ defmodule ${Name}.Config do
88
+ @config_data ${elixirString(configJson)}
89
+
90
+ def make_config do
91
+ ${Name}.Json.parse(@config_data)
92
+ end
93
+
94
+ # SHARED CONFIG (sdkgen rung L2).
95
+ #
96
+ # The SDK reads the config on every request and never writes to it, so one
97
+ # instance is shared by every client rather than rebuilt per client. Above the
98
+ # size threshold make_config re-parses the whole embedded JSON, so this is the
99
+ # difference between parsing the model once and once per client.
100
+ #
101
+ # :persistent_term because struct nodes are ETS-backed handles: the stored
102
+ # value is the handle, so every caller gets the same nodes. A concurrent first
103
+ # call may build twice and the last write wins - both results are valid
104
+ # configs, so the race is benign.
105
+ @shared_key {__MODULE__, :shared_config}
106
+
107
+ # The process-wide config, built once on first use.
108
+ #
109
+ # The returned node is SHARED: treat it as read-only. Callers that need to
110
+ # mutate should use make_config, which always returns a fresh copy.
111
+ #
112
+ # VALIDATED ON READ, and this is not belt-and-braces. The struct heap is a
113
+ # named ETS table created with no heir, so it is owned by whichever process
114
+ # first touched struct. If that was a short-lived one - a Task, a request, an
115
+ # ExUnit case - the table dies with it and every handle allocated in it goes
116
+ # stale. Caching a handle in :persistent_term makes that permanent: without
117
+ # this check the SDK hands out the dead handle for the life of the VM and
118
+ # every getprop raises ArgumentError. Reproduced:
119
+ #
120
+ # cached inside a task: {:vmap, 144}
121
+ # heap alive after task exit: :undefined
122
+ # getprop RAISED: ArgumentError
123
+ #
124
+ # Rebuilding on a dead handle costs one parse and restores exactly the
125
+ # pre-L2 behaviour, so the failure degrades to "no sharing" rather than to a
126
+ # broken SDK. The real fix is a durable owner for the heap, which belongs in
127
+ # the struct port rather than here.
128
+ def shared_config do
129
+ cached = :persistent_term.get(@shared_key, nil)
130
+
131
+ if cached != nil and usable?(cached) do
132
+ cached
133
+ else
134
+ cfg = make_config()
135
+ :persistent_term.put(@shared_key, cfg)
136
+ cfg
137
+ end
138
+ end
139
+
140
+ # Is this handle still backed by a live heap? Asked through the public API
141
+ # rather than by inspecting the table, so it stays correct if struct changes
142
+ # how nodes are stored.
143
+ defp usable?(cfg) do
144
+ Voxgig.Struct.getprop(cfg, "main")
145
+ true
146
+ rescue
147
+ ArgumentError -> false
148
+ end
149
+ end
150
+ `)
151
+ return
152
+ }
153
+
58
154
  Content(`# ${Name} SDK configuration
59
155
  #
60
156
  # Returns the resolved SDK config as vendored-struct nodes (via
@@ -72,23 +168,42 @@ defmodule ${Name}.Config do
72
168
  `)
73
169
  })
74
170
 
171
+ // `options` rendered WHOLE from the canonical definition rather than
172
+ // assembled slot by slot. Assembling it meant `options.server` - the
173
+ // OpenAPI server-variable defaults - was simply absent from this branch,
174
+ // so a templated server URL described a different config either side of
175
+ // the threshold. Same fix as dart, rust and zig.
75
176
  Content(` },
76
- "options" => %{
77
- "base" => ${elixirString(baseUrl)},
78
- ${authBlock} "headers" => ${formatElixir(headers, 4)},
79
- "entity" => %{
80
- `)
81
-
82
- each(entity, (e: any) => {
83
- Content(` ${elixirString(e.name)} => %{},
84
- `)
85
- })
86
-
87
- Content(` }
88
- },
177
+ "options" => ${formatElixir(configDef.options, 3)},
89
178
  "entity" => ${formatElixir(entityClean, 3)}
90
179
  })
91
180
  end
181
+
182
+ # SHARED CONFIG (sdkgen rung L2). See the data branch for the rationale, and
183
+ # for why the cached handle is validated on read.
184
+ @shared_key {__MODULE__, :shared_config}
185
+
186
+ # The process-wide config, built once on first use. The returned node is
187
+ # SHARED: treat it as read-only. Callers that need to mutate should use
188
+ # make_config, which always returns a fresh copy.
189
+ def shared_config do
190
+ cached = :persistent_term.get(@shared_key, nil)
191
+
192
+ if cached != nil and usable?(cached) do
193
+ cached
194
+ else
195
+ cfg = make_config()
196
+ :persistent_term.put(@shared_key, cfg)
197
+ cfg
198
+ end
199
+ end
200
+
201
+ defp usable?(cfg) do
202
+ Voxgig.Struct.getprop(cfg, "main")
203
+ true
204
+ rescue
205
+ ArgumentError -> false
206
+ end
92
207
  end
93
208
  `)
94
209
  })
@@ -18,7 +18,9 @@ defmodule ProjectName do
18
18
  utility = Utility.new()
19
19
  S.setprop(client, "_utility", utility)
20
20
 
21
- config = ProjectName.Config.make_config()
21
+ # The process-wide config (sdkgen rung L2): read-only on the request path,
22
+ # so every client shares one rather than rebuilding it.
23
+ config = ProjectName.Config.shared_config()
22
24
 
23
25
  rootctx =
24
26
  Context.new(
@@ -91,6 +91,12 @@ function elixirString(s: string): string {
91
91
  return '"' + String(s)
92
92
  .replace(/\\/g, '\\\\')
93
93
  .replace(/"/g, '\\"')
94
+ // `#` escaped because Elixir INTERPOLATES `\#{...}` inside a double-quoted
95
+ // string. Without this a model value containing `#{` is evaluated as code
96
+ // at compile time rather than emitted as text - which is both a wrong
97
+ // value and an arbitrary-expression hole. Escaping every `#` is simpler to
98
+ // reason about than escaping only `#{`, and `\#` is just `#` in Elixir.
99
+ .replace(/#/g, '\\#')
94
100
  .replace(/\n/g, '\\n')
95
101
  .replace(/\t/g, '\\t')
96
102
  + '"'
@@ -2,22 +2,20 @@ import {
2
2
  Content,
3
3
  File,
4
4
  cmp,
5
- each,
6
- isAuthActive,
7
- resolveAuthPrefix,
5
+ configDefinition,
6
+ configReprSetting,
7
+ isConfigData,
8
8
  } from '@voxgig/sdkgen'
9
9
 
10
10
 
11
11
  import {
12
- KIT,
13
12
  Model,
14
- getModelPath,
15
13
  } from '@voxgig/apidef'
16
14
 
17
15
 
18
16
  import {
19
- clean,
20
17
  formatHsValue,
18
+ hsString,
21
19
  } from './utility_haskell'
22
20
 
23
21
 
@@ -38,50 +36,36 @@ const Config = cmp(async function Config(props: any) {
38
36
 
39
37
  const model: Model = ctx$.model
40
38
 
41
- const entity = getModelPath(model, `main.${KIT}.entity`)
42
- const feature = getModelPath(model, `main.${KIT}.feature`)
39
+ // The same config as an OBJECT, built by the shared helper so this target's
40
+ // literal and the data that replaces it above the threshold are the same
41
+ // config by construction. The JSON is what the threshold is measured on -
42
+ // emitted source size varies by language, the model does not.
43
+ const { def: configDef, json: configJson } = configDefinition(model)
44
+ const asData = isConfigData(configJson, configReprSetting(model))
45
+
46
+ // ABOVE THE THRESHOLD: emit the model as DATA.
47
+ //
48
+ // The literal representation is one enormous CV expression. GHC type-checks
49
+ // and desugars every constructor application in it, and buildCV then walks
50
+ // the whole tree allocating an IORef per node. A string constant is one
51
+ // token, and jsonRead builds the same Value from it directly.
52
+ //
53
+ // Both branches return a FRESH Value per call - buildCV allocates new
54
+ // IORefs, jsonRead re-parses - so a caller that mutates one config cannot
55
+ // reach another's, exactly as before.
56
+ const configBody = asData
57
+ ? `-- THE API MODEL, EMBEDDED AS DATA (sdkgen rung L1).
58
+ --
59
+ -- Emitted only above a size threshold, or when \`main.kit.config.repr\` pins
60
+ -- it: for a small model the CV literal is smaller and far easier to read when
61
+ -- debugging.
62
+ configData :: String
63
+ configData = ${hsString(configJson)}
43
64
 
44
- const headers = getModelPath(model, `main.${KIT}.config.headers`) || {}
45
-
46
- const authActive = isAuthActive(model)
47
- const authPrefix = resolveAuthPrefix(model)
48
-
49
- let baseUrl = ''
50
- try { baseUrl = getModelPath(model, `main.${KIT}.info.servers.0.url`) } catch (_e) { }
51
-
52
- const featureConfig: any = {}
53
- each(feature, (f: any) => {
54
- featureConfig[f.name] = f.config || {}
55
- })
56
-
57
- const entityOptions: any = {}
58
- each(entity, (ent: any) => {
59
- entityOptions[ent.name] = {}
60
- })
61
-
62
- const options: any = {
63
- base: baseUrl,
64
- headers,
65
- entity: entityOptions,
66
- }
67
- if (authActive) {
68
- options.auth = { prefix: authPrefix }
69
- }
70
-
71
- const entityConfig = Object.values(entity || {}).reduce((a: any, n: any) => (
72
- a[n.name] = clean({
73
- fields: n.fields,
74
- name: n.name,
75
- op: n.op,
76
- relations: n.relations,
77
- }, true), a), {})
78
-
79
- const config = {
80
- main: { name: model.const.Name },
81
- feature: featureConfig,
82
- options,
83
- entity: entityConfig,
84
- }
65
+ makeConfig :: IO Value
66
+ makeConfig = jsonRead configData`
67
+ : `makeConfig :: IO Value
68
+ makeConfig = buildCV ${formatHsValue(configDef)}`
85
69
 
86
70
  File({ name: 'SdkConfig.' + target.ext }, () => {
87
71
 
@@ -90,12 +74,11 @@ const Config = cmp(async function Config(props: any) {
90
74
  module SdkConfig (makeConfig, makeFeature) where
91
75
 
92
76
  import VoxgigStruct (Value)
93
- import SdkHelpers (CV (..), buildCV)
77
+ import ${asData ? 'SdkJson (jsonRead)' : 'SdkHelpers (CV (..), buildCV)'}
94
78
  import SdkTypes (Feature)
95
79
  import qualified SdkFeatures as F
96
80
 
97
- makeConfig :: IO Value
98
- makeConfig = buildCV ${formatHsValue(config)}
81
+ ${configBody}
99
82
 
100
83
  makeFeature :: String -> IO Feature
101
84
  makeFeature name = case name of
@@ -39,6 +39,7 @@ library
39
39
  hs-source-dirs: src
40
40
  exposed-modules: VoxgigStruct
41
41
  Vregex
42
+ SdkJson
42
43
  SdkTypes
43
44
  SdkHelpers
44
45
  SdkRuntime
@@ -257,7 +257,7 @@ import qualified SdkFeatures as F
257
257
  import qualified SdkClient as C
258
258
  import qualified TReadmeExamples
259
259
  import Testutil
260
- import TestJson (jsonRead)
260
+ import SdkJson (jsonRead)
261
261
 
262
262
  -- Load an entity fixture (../.sdk/test/entity/<name>/<Name>TestData.json).
263
263
  loadFixture :: String -> IO Value
@@ -54,9 +54,20 @@ function hsString(s: string): string {
54
54
 
55
55
 
56
56
  // Render a JSON-shaped value as a Haskell `CV` literal (the generated
57
- // SdkConfig realises it with buildCV). Keys sorted for byte-stable output;
58
- // empty map/list render as (CVMap []) / (CVList []) — valid for 0, 1 or N
59
- // entries (N-feature-safe).
57
+ // SdkConfig realises it with buildCV). Empty map/list render as
58
+ // (CVMap []) / (CVList []) — valid for 0, 1 or N entries (N-feature-safe).
59
+ //
60
+ // KEY ORDER IS INSERTION ORDER, not sorted. struct's Haskell `Value` holds a
61
+ // map as an ORDERED assoc list, so key order is observable — it survives into
62
+ // keysof, iteration and stringify. This used to sort, which was fine while
63
+ // this was the only representation, but above the size threshold the same
64
+ // config arrives via jsonRead in the JSON text's order. Sorting here would
65
+ // have made the two representations describe the same config in a different
66
+ // order, which is exactly what rung L1 promises cannot happen.
67
+ //
68
+ // Insertion order is still byte-stable: every nested level of the config comes
69
+ // from `each`, which iterates sorted, and the two levels built by hand
70
+ // (the root, and `options`) have a fixed order every target's literal follows.
60
71
  function formatHsValue(val: any): string {
61
72
  if (val === null || val === undefined) {
62
73
  return 'CVNull'
@@ -75,8 +86,7 @@ function formatHsValue(val: any): string {
75
86
  return '(CVList [' + items + '])'
76
87
  }
77
88
  if (typeof val === 'object') {
78
- const keys = Object.keys(val).sort()
79
- const items = keys
89
+ const items = Object.keys(val)
80
90
  .map((k) => '(' + hsString(k) + ', ' + formatHsValue(val[k]) + ')')
81
91
  .join(', ')
82
92
  return '(CVMap [' + items + '])'
@@ -101,6 +101,27 @@ public final class Config {
101
101
  return (Map<String, Object>) Json.parse(configJson());
102
102
  }
103
103
 
104
+ // SHARED CONFIG (sdkgen rung L2).
105
+ //
106
+ // The SDK reads the config on every request and never writes to it, so one
107
+ // instance is shared by every client rather than rebuilt per client - the
108
+ // difference between parsing the embedded JSON once and once per client.
109
+ //
110
+ // Initialization-on-demand holder: the JLS guarantees the class initializer
111
+ // runs once, lazily, and safely under concurrency, with no locking on the
112
+ // read path.
113
+ private static final class SharedHolder {
114
+ static final Map<String, Object> VALUE = makeConfig();
115
+ }
116
+
117
+ // The process-wide config, built once on first use.
118
+ //
119
+ // The returned map is SHARED: treat it as read-only. Callers that need to
120
+ // mutate should use makeConfig, which always parses a fresh copy.
121
+ public static Map<String, Object> sharedConfig() {
122
+ return SharedHolder.VALUE;
123
+ }
124
+
104
125
  public static Feature makeFeature(String name) {
105
126
  switch (name) {
106
127
  `)
@@ -93,6 +93,21 @@ object Config {
93
93
  return Json.parse(configJson()) as MutableMap<String, Any?>
94
94
  }
95
95
 
96
+ // SHARED CONFIG (sdkgen rung L2).
97
+ //
98
+ // The SDK reads the config on every request and never writes to it, so one
99
+ // instance is shared by every client rather than rebuilt per client - the
100
+ // difference between parsing the embedded JSON once and once per client.
101
+ //
102
+ // 'by lazy' defaults to LazyThreadSafetyMode.SYNCHRONIZED, so concurrent
103
+ // first calls build it exactly once.
104
+ //
105
+ // The returned map is SHARED: treat it as read-only. Callers that need to
106
+ // mutate should use makeConfig, which always parses a fresh copy.
107
+ private val sharedConfigVal: MutableMap<String, Any?> by lazy { makeConfig() }
108
+
109
+ fun sharedConfig(): MutableMap<String, Any?> = sharedConfigVal
110
+
96
111
  fun makeFeature(name: String): Feature {
97
112
  return when (name) {
98
113
  `)