@voxgig/station 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Station.d.ts +1 -118
- package/dist/src/Station.js +26 -412
- package/dist/src/Station.js.map +1 -1
- package/dist/src/adapter.js +0 -54
- package/dist/src/adapter.js.map +1 -1
- package/dist/src/config-shape.js +0 -11
- package/dist/src/config-shape.js.map +1 -1
- package/dist/src/descriptor.js +0 -38
- package/dist/src/descriptor.js.map +1 -1
- package/dist/src/error.js +0 -5
- package/dist/src/error.js.map +1 -1
- package/dist/src/factory.d.ts +0 -9
- package/dist/src/factory.js +0 -37
- package/dist/src/factory.js.map +1 -1
- package/dist/src/feature.d.ts +0 -75
- package/dist/src/feature.js +0 -87
- package/dist/src/feature.js.map +1 -1
- package/dist/src/index.js +0 -7
- package/dist/src/index.js.map +1 -1
- package/dist/src/loader.d.ts +0 -29
- package/dist/src/loader.js +0 -66
- package/dist/src/loader.js.map +1 -1
- package/dist/src/profile.js +0 -91
- package/dist/src/profile.js.map +1 -1
- package/dist/src/secrets.d.ts +5 -0
- package/dist/src/secrets.js +106 -32
- package/dist/src/secrets.js.map +1 -1
- package/dist/src/shape.d.ts +0 -22
- package/dist/src/shape.js +0 -113
- package/dist/src/shape.js.map +1 -1
- package/package.json +3 -3
- package/src/Station.ts +28 -413
- package/src/adapter.ts +0 -57
- package/src/config-shape.ts +0 -11
- package/src/descriptor.ts +0 -38
- package/src/error.ts +0 -5
- package/src/factory.ts +0 -37
- package/src/feature.ts +0 -87
- package/src/index.ts +0 -7
- package/src/loader.ts +0 -66
- package/src/profile.ts +0 -95
- package/src/secrets.ts +104 -32
- package/src/shape.ts +0 -113
- package/src/types.ts +0 -59
package/src/adapter.ts
CHANGED
|
@@ -1,34 +1,16 @@
|
|
|
1
1
|
import { StationError } from './error'
|
|
2
2
|
import { Station } from './Station'
|
|
3
3
|
|
|
4
|
-
// The station side of the plugin contract (design §3), in ONE place:
|
|
5
|
-
// featureBinding() is what both entry paths delegate to -
|
|
6
|
-
// - the GENERATED station feature (sdkgen-station's ts template) calls
|
|
7
|
-
// it from its init() and forwards its hook methods;
|
|
8
|
-
// - the library's carried adapter (adapterFeature, the adopt/connect
|
|
9
|
-
// retrofit for SDKs generated without the feature) is a thin shell
|
|
10
|
-
// over the same call.
|
|
11
|
-
// Registration at init, wrap position verified, transport wrapped with
|
|
12
|
-
// copy-on-inject, hooks bridged to op events. Anything changed here
|
|
13
|
-
// changes both paths - which is the point.
|
|
14
4
|
|
|
15
5
|
let corrSeq = 0
|
|
16
6
|
|
|
17
7
|
export type FeatureBinding = {
|
|
18
|
-
// The INSTANCE name (§7.1). Keeps the field name so the generated
|
|
19
|
-
// adapter contract is unchanged; for a single-instance project it is
|
|
20
|
-
// the api slug, exactly as before.
|
|
21
8
|
slug: string
|
|
22
9
|
PrePoint(ctx: any): void
|
|
23
10
|
PreDone(ctx: any): void
|
|
24
11
|
PreUnexpected(ctx: any): void
|
|
25
12
|
}
|
|
26
13
|
|
|
27
|
-
// Resolve the station this activation binds to: an explicit handle in
|
|
28
|
-
// the feature options (connect/adopt and st.options() pass one), else
|
|
29
|
-
// the ambient instance. No station open -> null: an activated feature
|
|
30
|
-
// with no opened station is an inert no-op that emits nothing and
|
|
31
|
-
// fails nothing (design §3.1).
|
|
32
14
|
export function featureBinding(ctx: any, fopts: any): FeatureBinding | null {
|
|
33
15
|
const station: Station | null =
|
|
34
16
|
(fopts?.station instanceof Station ? fopts.station : null) ?? Station.current()
|
|
@@ -36,18 +18,11 @@ export function featureBinding(ctx: any, fopts: any): FeatureBinding | null {
|
|
|
36
18
|
|
|
37
19
|
const client = ctx.client
|
|
38
20
|
|
|
39
|
-
// Same construction, second arrival (generated feature + carried
|
|
40
|
-
// adapter both active on one client): the first bind won, this one
|
|
41
|
-
// is inert. See Station._boundEntry.
|
|
42
21
|
if (null != station._boundEntry(client)) { return null }
|
|
43
22
|
const utility = ctx.utility
|
|
44
23
|
const options = ctx.options
|
|
45
24
|
const calleropts = fopts?.calleropts
|
|
46
25
|
|
|
47
|
-
// Position guard (design §3.3): the wrap must sit immediately
|
|
48
|
-
// outside the base transport - inside retry/cache/ratelimit - or
|
|
49
|
-
// its http events stop being wire truth. Position in
|
|
50
|
-
// client._features IS init order, so verify it and fail loudly.
|
|
51
26
|
const names = client._features.map((f: any) => f.name)
|
|
52
27
|
const self = names.indexOf('station')
|
|
53
28
|
const testAt = names.indexOf('test')
|
|
@@ -58,15 +33,8 @@ export function featureBinding(ctx: any, fopts: any): FeatureBinding | null {
|
|
|
58
33
|
'feature order is [' + names.join(', ') + ']')
|
|
59
34
|
}
|
|
60
35
|
|
|
61
|
-
// §7.5: registration is driven by station now. `fopts.instance` is
|
|
62
|
-
// where station puts the instance name it knew before construction
|
|
63
|
-
// began; `_register` reads it and falls back to the descriptor slug,
|
|
64
|
-
// which is today's behaviour for a bare connect(SDK).
|
|
65
36
|
const reg = station._register(client, ctx.config, options, calleropts, fopts)
|
|
66
37
|
const { binding, profilePlugin } = reg
|
|
67
|
-
// The INSTANCE name, not the api slug. Everything below keys on it -
|
|
68
|
-
// the placeholder, the transport seam, the op events - because two
|
|
69
|
-
// live instances of one api must be distinguishable at each.
|
|
70
38
|
const name = binding.plugin
|
|
71
39
|
|
|
72
40
|
// Base URL precedence (design §3.5): caller opts (7) beat the
|
|
@@ -78,18 +46,6 @@ export function featureBinding(ctx: any, fopts: any): FeatureBinding | null {
|
|
|
78
46
|
options.base = profilePlugin.base
|
|
79
47
|
}
|
|
80
48
|
|
|
81
|
-
// Policy allowlists (design §16): `allow.op` / `allow.method` are
|
|
82
|
-
// "the same vocabulary the SDKs already enforce (`options.allow`, and
|
|
83
|
-
// the raw-access gate every target implements); station sets these
|
|
84
|
-
// SDK options from policy so enforcement is in the SDK's own
|
|
85
|
-
// pipeline" - the point_op_allow / spec_method_allow gates. The SDK's
|
|
86
|
-
// own option form is a comma-separated string, so the policy's list
|
|
87
|
-
// joins into it. Applied at binding time, which is inside the
|
|
88
|
-
// constructor, and on BOTH entry paths - connect/adopt and the
|
|
89
|
-
// declarative build both delegate here. Unlike `base` above, which is
|
|
90
|
-
// a default the caller may override, an allowlist is ENFORCEMENT:
|
|
91
|
-
// policy wins over whatever the options carry, on exactly the keys it
|
|
92
|
-
// sets.
|
|
93
49
|
const pallow = profilePlugin?.policy?.allow
|
|
94
50
|
if (null != pallow && 'object' === typeof pallow) {
|
|
95
51
|
const allow = {
|
|
@@ -104,10 +60,6 @@ export function featureBinding(ctx: any, fopts: any): FeatureBinding | null {
|
|
|
104
60
|
if ('none' !== binding.rung) {
|
|
105
61
|
const placeholder = binding.placeholder!
|
|
106
62
|
|
|
107
|
-
// A real credential already resident in the options is hoisted
|
|
108
|
-
// into the broker and replaced by the placeholder before
|
|
109
|
-
// construction completes (design §3.1 adopt) - options() and
|
|
110
|
-
// prepare() output become placeholder-safe from here on.
|
|
111
63
|
const resident = options.apikey
|
|
112
64
|
if ('string' === typeof resident && '' !== resident && placeholder !== resident) {
|
|
113
65
|
station._hoist(name, resident)
|
|
@@ -115,9 +67,6 @@ export function featureBinding(ctx: any, fopts: any): FeatureBinding | null {
|
|
|
115
67
|
options.apikey = placeholder
|
|
116
68
|
}
|
|
117
69
|
|
|
118
|
-
// Wrap the transport. Copy-on-inject (design §5.3) happens inside
|
|
119
|
-
// station._transport; auth-inactive plugins skip credential
|
|
120
|
-
// planning but the wrap still observes (design §5.3).
|
|
121
70
|
const inner = utility.fetcher
|
|
122
71
|
if (true === (inner as any).__station__) {
|
|
123
72
|
throw new StationError('station_bound_twice',
|
|
@@ -132,8 +81,6 @@ export function featureBinding(ctx: any, fopts: any): FeatureBinding | null {
|
|
|
132
81
|
return {
|
|
133
82
|
slug: name,
|
|
134
83
|
|
|
135
|
-
// Hook bridge (design §3 item 3): operation semantics correlated
|
|
136
|
-
// with the HTTP events via a per-operation id on the SDK's own ctx.
|
|
137
84
|
PrePoint(opctx: any): void {
|
|
138
85
|
opctx.station$ = { corr: 'c' + (++corrSeq), start: Date.now() }
|
|
139
86
|
},
|
|
@@ -158,10 +105,6 @@ export function adapterFeature(station: Station, calleropts: any): any {
|
|
|
158
105
|
active: true,
|
|
159
106
|
_binding: null as FeatureBinding | null,
|
|
160
107
|
|
|
161
|
-
// featureAdd reads _options for positioning: immediately after the
|
|
162
|
-
// test feature's base transport (design §3.3). When test is absent
|
|
163
|
-
// from the add order this is a no-op append, which for a bare SDK
|
|
164
|
-
// still lands the wrap immediately outside the base transport.
|
|
165
108
|
_options: { __after__: 'test' },
|
|
166
109
|
|
|
167
110
|
init(ctx: any, fopts: any): void {
|
package/src/config-shape.ts
CHANGED
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/* MIRROR of `spec/config-shape.json` - design §4.3, verbatim.
|
|
2
|
-
*
|
|
3
|
-
* The JSON under `spec/` is the artifact every port reads; this module
|
|
4
|
-
* is the TypeScript port's shipped copy of it. It exists because
|
|
5
|
-
* `package.json` ships `dist/src` only, so a runtime read of `spec/`
|
|
6
|
-
* works in the repo and breaks in a published consumer - and
|
|
7
|
-
* `validateConfig` runs at `open()`, not just under test.
|
|
8
|
-
*
|
|
9
|
-
* DO NOT EDIT THIS FILE. Edit `spec/config-shape.json`, then re-run
|
|
10
|
-
* `make sync-shape`. `test/shape.test.ts` fails on drift.
|
|
11
|
-
*/
|
|
12
1
|
|
|
13
2
|
export const CONFIG_SHAPE: any = {
|
|
14
3
|
"station": [
|
package/src/descriptor.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { Descriptor, DescriptorEntity, DescriptorPoint } from './types'
|
|
2
2
|
|
|
3
|
-
// The ONLY way to build an env-var token in station, mirroring sdkgen's
|
|
4
|
-
// packageMeta envToken exactly: 'gnarly-pets' -> 'GNARLY_PETS'. The
|
|
5
|
-
// `secretname` corpus section pins the round-trip against sekreto's
|
|
6
|
-
// envkey() and sdkgen's envName() - the one place three grammars meet.
|
|
7
3
|
export function envtoken(name: any): string {
|
|
8
4
|
return String(name || '')
|
|
9
5
|
.toUpperCase().replace(/[^A-Z0-9]+/g, '_').replace(/^_+|_+$/g, '')
|
|
@@ -16,10 +12,6 @@ export function secretnameDefault(slug: string): string {
|
|
|
16
12
|
return envtoken(slug).toLowerCase() + '.apikey'
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
// Best-effort slug from a camel name, for SDKs whose embedded config
|
|
20
|
-
// predates main.slug (design §4 legacy sentinels). The hyphen caveat is
|
|
21
|
-
// real: 'VoxgigSolardemo' -> 'voxgigsolardemo', NOT 'voxgig-solardemo' -
|
|
22
|
-
// callers surface a warning event when this path is taken.
|
|
23
15
|
function legacySlug(name: string): string {
|
|
24
16
|
return String(name || '').toLowerCase()
|
|
25
17
|
}
|
|
@@ -90,20 +82,6 @@ export function normalizeDescriptor(config: any, activeFeatures?: Record<string,
|
|
|
90
82
|
entities[ename] = { fields, ops }
|
|
91
83
|
}
|
|
92
84
|
|
|
93
|
-
// §7.4: the features list gains `options` and `transport`, because
|
|
94
|
-
// it was throwing away what the SDK already embeds -
|
|
95
|
-
// `config.feature[name].options` is the feature's own declared key
|
|
96
|
-
// set WITH TYPED DEFAULTS, which is the schema §8.5 validates
|
|
97
|
-
// against, and `transport` is the role §8.4 orders by.
|
|
98
|
-
//
|
|
99
|
-
// Both are already inside the SDK; the descriptor stops discarding
|
|
100
|
-
// them. ADDITIVE, so descriptor v1 consumers are unaffected.
|
|
101
|
-
//
|
|
102
|
-
// `transport` is carried rather than inferred: the obvious signal, an
|
|
103
|
-
// empty `hook: {}`, is wrong for station, which both wraps AND
|
|
104
|
-
// dispatches hooks. Absent until sdkgen emits it (§11 items 6-7), and
|
|
105
|
-
// §8.4's role checks degrade to nothing until then rather than
|
|
106
|
-
// guessing.
|
|
107
85
|
const features: {
|
|
108
86
|
name: string, active: boolean,
|
|
109
87
|
options?: Record<string, any>, transport?: string,
|
|
@@ -134,21 +112,6 @@ export function normalizeDescriptor(config: any, activeFeatures?: Record<string,
|
|
|
134
112
|
return { descriptor, warnings }
|
|
135
113
|
}
|
|
136
114
|
|
|
137
|
-
// Canonical serialization (design §4): UTF-8, object keys sorted
|
|
138
|
-
// bytewise, no insignificant whitespace, minimal JSON escaping. The
|
|
139
|
-
// proxy dedupes registrations by a hash of this, so every language must
|
|
140
|
-
// produce identical bytes - the `canonical-serialize` corpus section
|
|
141
|
-
// carries the adversarial cases.
|
|
142
|
-
//
|
|
143
|
-
// The bytewise sort compares UTF-8 BYTE SEQUENCES, not UTF-16 code
|
|
144
|
-
// units, through TextEncoder rather than Buffer: TextEncoder is a
|
|
145
|
-
// standard global, so this module carries no node: dependency. (That
|
|
146
|
-
// last point used to matter because a browser entry had to reach this
|
|
147
|
-
// module; station is server-side only now. TextEncoder stays because
|
|
148
|
-
// the behaviour below is what the corpus pins, not because of where it
|
|
149
|
-
// runs.) Uint8Array elements are unsigned bytes, so plain numeric
|
|
150
|
-
// comparison IS the bytewise order Buffer.compare gave - the corpus'
|
|
151
|
-
// non-ASCII sort case pins the equivalence.
|
|
152
115
|
const UTF8 = new TextEncoder()
|
|
153
116
|
|
|
154
117
|
function bytecompare(a: string, b: string): number {
|
|
@@ -172,7 +135,6 @@ export function canonicalSerialize(value: any): string {
|
|
|
172
135
|
}
|
|
173
136
|
if ('object' === typeof value) {
|
|
174
137
|
const keys = Object.keys(value).filter((k) => undefined !== value[k])
|
|
175
|
-
// Bytewise sort: compare UTF-8 byte sequences, not UTF-16 code units.
|
|
176
138
|
keys.sort(bytecompare)
|
|
177
139
|
return '{' + keys.map((k) =>
|
|
178
140
|
JSON.stringify(k) + ':' + canonicalSerialize(value[k])).join(',') + '}'
|
package/src/error.ts
CHANGED
|
@@ -20,11 +20,6 @@ const CODES = [
|
|
|
20
20
|
'station_open_conflict',
|
|
21
21
|
'station_bound_twice',
|
|
22
22
|
|
|
23
|
-
// Declarative config (station-declarative-config.md §6.4). Stage 1
|
|
24
|
-
// raises the first three; `station_feature_reserved` is the pure-data
|
|
25
|
-
// half of §8.4/§8.6, checkable before feature.ts exists because both
|
|
26
|
-
// rules are lexical - a `station` key in a feature map, a `feature`
|
|
27
|
-
// key inside `options`.
|
|
28
23
|
'station_config_invalid',
|
|
29
24
|
'station_config_secret',
|
|
30
25
|
'station_secret_collision',
|
package/src/factory.ts
CHANGED
|
@@ -1,31 +1,3 @@
|
|
|
1
|
-
/* The factory table (design §6.2).
|
|
2
|
-
*
|
|
3
|
-
* A FACTORY IS A CONSTRUCTOR *PLUS* THE SDK'S STATIC CONFIG, not a bare
|
|
4
|
-
* function, and leaving the second half out was a hole in the first
|
|
5
|
-
* draft of §6.2.
|
|
6
|
-
*
|
|
7
|
-
* Station composes the ordered feature array FOR the constructor, so it
|
|
8
|
-
* needs the transport roles and the feature option schemas BEFORE
|
|
9
|
-
* construction — but §7.5 has the adapter build and register its
|
|
10
|
-
* descriptor DURING construction. Nothing would be known in time.
|
|
11
|
-
*
|
|
12
|
-
* The config is available, though: `configDefinition` emits it as a
|
|
13
|
-
* module-level constant in the generated package, so it exists as soon
|
|
14
|
-
* as the package is linked and long before any instance is built.
|
|
15
|
-
* Station normalizes the descriptor AT PROVIDE TIME, and three things
|
|
16
|
-
* follow:
|
|
17
|
-
*
|
|
18
|
-
* - §7.4's per-api descriptor cache is populated at registration
|
|
19
|
-
* rather than on first construction;
|
|
20
|
-
* - `check()` can validate every instance's feature config WITHOUT
|
|
21
|
-
* constructing anything;
|
|
22
|
-
* - the adapter's registration during construction becomes a
|
|
23
|
-
* reconciliation — same descriptor, now bound to a live client —
|
|
24
|
-
* rather than the first sighting.
|
|
25
|
-
*
|
|
26
|
-
* The table is PROCESS-GLOBAL because path 1 of §6.2 is module
|
|
27
|
-
* self-registration: a generated package registers itself when it is
|
|
28
|
-
* linked, which happens once per process and not once per Station. */
|
|
29
1
|
|
|
30
2
|
import { StationError } from './error'
|
|
31
3
|
import { normalizeDescriptor } from './descriptor'
|
|
@@ -45,13 +17,6 @@ export type FactoryEntry = {
|
|
|
45
17
|
|
|
46
18
|
const TABLE = new Map<string, FactoryEntry>()
|
|
47
19
|
|
|
48
|
-
/** Register an api's `{construct, config}` pair.
|
|
49
|
-
*
|
|
50
|
-
* Idempotent per api: registering the SAME pair twice is a no-op,
|
|
51
|
-
* because module self-registration and an explicit `provide` for one
|
|
52
|
-
* api is an ordinary thing for an application to end up with. A second
|
|
53
|
-
* registration with a DIFFERENT factory is `station_factory_conflict` —
|
|
54
|
-
* silently picking one of two SDK builds is not a thing to do quietly. */
|
|
55
20
|
export function provide(api: string, factory: Factory): FactoryEntry {
|
|
56
21
|
const slug = String(api)
|
|
57
22
|
const prior = TABLE.get(slug)
|
|
@@ -86,8 +51,6 @@ export function provided(): string[] {
|
|
|
86
51
|
return Array.from(TABLE.keys()).sort()
|
|
87
52
|
}
|
|
88
53
|
|
|
89
|
-
/** Test seam. The table is process-global by design, so a suite that
|
|
90
|
-
* registers factories has to be able to put the process back. */
|
|
91
54
|
export function resetFactories(): void {
|
|
92
55
|
TABLE.clear()
|
|
93
56
|
}
|
package/src/feature.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/* Feature management (design §8): the three-level merge, the
|
|
2
|
-
* constraint-and-band resolver, and the descriptor-derived checker.
|
|
3
|
-
*
|
|
4
|
-
* The resolver is written to voxgig/plugin's §7 semantics so plugin can
|
|
5
|
-
* extract it — this is one of the pieces the joint plan means by
|
|
6
|
-
* "station builds natively to plugin's semantics". Keeping the two
|
|
7
|
-
* spellings identical is what C4 holds station to. */
|
|
8
1
|
|
|
9
2
|
import { StationError } from './error'
|
|
10
3
|
|
|
@@ -12,31 +5,8 @@ import { StationError } from './error'
|
|
|
12
5
|
// §8.3 — the merge
|
|
13
6
|
// ---------------------------------------------------------------------
|
|
14
7
|
|
|
15
|
-
/** Reserved on a feature entry: not options, and never passed through
|
|
16
|
-
* to the SDK's own option map. */
|
|
17
8
|
export const RESERVED_KEYS = ['active', 'order']
|
|
18
9
|
|
|
19
|
-
/** The six sources, in §3.3's order extended by the profile level:
|
|
20
|
-
*
|
|
21
|
-
* 1 base.feature 4 overlay.feature
|
|
22
|
-
* 2 base.api[<api>].feature 5 overlay.api[<api>].feature
|
|
23
|
-
* 3 base.sdk[<ref>].feature 6 overlay.sdk[<ref>].feature
|
|
24
|
-
*
|
|
25
|
-
* PROFILE SPECIFICITY OUTRANKS BLOCK SPECIFICITY, and within a profile
|
|
26
|
-
* the narrower block wins — the same principle as §3.3, one level down.
|
|
27
|
-
*
|
|
28
|
-
* `feature` is the ONE key where §3.3's shallow-per-key rule is wrong:
|
|
29
|
-
* composition is the entire point, a fleet default plus a per-instance
|
|
30
|
-
* tweak. So it is a TWO-LEVEL merge — per feature name, then per option
|
|
31
|
-
* key — and no deeper. A map-valued option REPLACES wholesale, which is
|
|
32
|
-
* what `{"$MERGE": {"deep": 2}}` states and what a port defaulting to a
|
|
33
|
-
* deep merge would silently get wrong.
|
|
34
|
-
*
|
|
35
|
-
* Same defaults-after-merge rule as §3.3, one level down: an entry
|
|
36
|
-
* mentioned at one level with only a tuning key must NOT synthesize
|
|
37
|
-
* `active` and switch on a feature a broader level turned off. That is
|
|
38
|
-
* the §3.3 defect one level down, and it is why the caller passes RAW
|
|
39
|
-
* blocks here. */
|
|
40
10
|
export function mergefeatures(sources: any[]): { [name: string]: any } {
|
|
41
11
|
const out: { [name: string]: any } = {}
|
|
42
12
|
for (const src of sources) {
|
|
@@ -44,15 +14,12 @@ export function mergefeatures(sources: any[]): { [name: string]: any } {
|
|
|
44
14
|
for (const name of Object.keys(src)) {
|
|
45
15
|
const entry = src[name]
|
|
46
16
|
if (!ismap(entry)) { out[name] = entry; continue }
|
|
47
|
-
// Per option key, and NOT deeper.
|
|
48
17
|
out[name] = { ...(ismap(out[name]) ? out[name] : {}), ...entry }
|
|
49
18
|
}
|
|
50
19
|
}
|
|
51
20
|
return out
|
|
52
21
|
}
|
|
53
22
|
|
|
54
|
-
/** The six sources for one instance, in order. Assembled here rather
|
|
55
|
-
* than at the call site so the order lives in exactly one place. */
|
|
56
23
|
export function featuresources(
|
|
57
24
|
base: any, overlay: any, api: string, ref: string
|
|
58
25
|
): any[] {
|
|
@@ -70,19 +37,10 @@ export function featuresources(
|
|
|
70
37
|
// §8.4 — activation and order
|
|
71
38
|
// ---------------------------------------------------------------------
|
|
72
39
|
|
|
73
|
-
/** `test` substitutes the base transport, so it takes the innermost
|
|
74
|
-
* band; `station` sits immediately outside it, pinned; everything else
|
|
75
|
-
* is band 0, outside station.
|
|
76
|
-
*
|
|
77
|
-
* THE DEFAULT IS TODAY'S BEHAVIOUR EXPRESSED IN THE NEW MODEL rather
|
|
78
|
-
* than as a special case: a project that writes no `order` anywhere
|
|
79
|
-
* sees exactly today's nesting, and sdkgen's two `makeOptions` special
|
|
80
|
-
* cases become two band values rather than two branches. */
|
|
81
40
|
export const BAND_DEFAULT = 0
|
|
82
41
|
export const BAND_STATION = 100
|
|
83
42
|
export const BAND_TEST = 200
|
|
84
43
|
|
|
85
|
-
/** Higher is further IN. */
|
|
86
44
|
export function defaultband(name: string): number {
|
|
87
45
|
if ('test' === name) { return BAND_TEST }
|
|
88
46
|
if ('station' === name) { return BAND_STATION }
|
|
@@ -91,20 +49,6 @@ export function defaultband(name: string): number {
|
|
|
91
49
|
|
|
92
50
|
export type Ordered = { name: string, band: number, entry: any }
|
|
93
51
|
|
|
94
|
-
/** Resolve the activation order: constraints, then bands, then the
|
|
95
|
-
* feature's position in the merged map.
|
|
96
|
-
*
|
|
97
|
-
* `before`/`after` take a feature name or a list of them and are
|
|
98
|
-
* SATISFIED VACUOUSLY when the named feature is absent — `after: 'test'`
|
|
99
|
-
* loads fine in a project with no test feature, which is sdkgen's
|
|
100
|
-
* `__after__` behaviour kept rather than reinvented.
|
|
101
|
-
*
|
|
102
|
-
* Constraints beat bands; bands break ties no constraint decides;
|
|
103
|
-
* remaining ties break by declaration position, so the result is a
|
|
104
|
-
* stable topological sort with no alphabetical accident in it.
|
|
105
|
-
*
|
|
106
|
-
* Returns OUTERMOST FIRST, which is the array form the constructor
|
|
107
|
-
* takes and the direction plugin's chain composes in. */
|
|
108
52
|
export function resolveorder(merged: { [name: string]: any }): Ordered[] {
|
|
109
53
|
const names = Object.keys(merged).filter((n) => active(merged[n]))
|
|
110
54
|
const pos = new Map<string, number>()
|
|
@@ -117,7 +61,6 @@ export function resolveorder(merged: { [name: string]: any }): Ordered[] {
|
|
|
117
61
|
band.set(n, b)
|
|
118
62
|
}
|
|
119
63
|
|
|
120
|
-
// edges: from OUTER to INNER. `after: X` means "further in than X".
|
|
121
64
|
const inner = new Map<string, Set<string>>()
|
|
122
65
|
for (const n of names) { inner.set(n, new Set()) }
|
|
123
66
|
|
|
@@ -127,7 +70,6 @@ export function resolveorder(merged: { [name: string]: any }): Ordered[] {
|
|
|
127
70
|
for (const n of names) {
|
|
128
71
|
const o = ismap(merged[n]) ? merged[n].order : undefined
|
|
129
72
|
if (!ismap(o)) { continue }
|
|
130
|
-
// Vacuous when absent: an unknown name is not an error here.
|
|
131
73
|
for (const other of listof(o.after)) {
|
|
132
74
|
if (inner.has(other)) { inner.get(other)!.add(n) }
|
|
133
75
|
}
|
|
@@ -173,23 +115,11 @@ export function resolveorder(merged: { [name: string]: any }): Ordered[] {
|
|
|
173
115
|
return out
|
|
174
116
|
}
|
|
175
117
|
|
|
176
|
-
/** A feature named in the config is one you are ASKING for, so an entry
|
|
177
|
-
* with no `active` is active. */
|
|
178
118
|
function active(entry: any): boolean {
|
|
179
119
|
if (!ismap(entry)) { return false !== entry }
|
|
180
120
|
return false !== entry.active
|
|
181
121
|
}
|
|
182
122
|
|
|
183
|
-
/** Station's own position is PINNED and not orderable (§8.4): an order
|
|
184
|
-
* that moves `station` away from immediately-outside-the-base is
|
|
185
|
-
* REJECTED, not honoured.
|
|
186
|
-
*
|
|
187
|
-
* The pin is `innermost`, and the spelling matters. A chain composes
|
|
188
|
-
* with the FIRST binding outermost, so a pin written in sort terms —
|
|
189
|
-
* "station first" — would place every other wrapper between the adapter
|
|
190
|
-
* and the base: the exact inversion of the invariant, and one that
|
|
191
|
-
* would leave station's wire-truth events observing the wrong boundary
|
|
192
|
-
* while still looking ordered. */
|
|
193
123
|
export function checkpin(ordered: Ordered[]): void {
|
|
194
124
|
const i = ordered.findIndex((o) => 'station' === o.name)
|
|
195
125
|
if (-1 === i) { return }
|
|
@@ -217,23 +147,6 @@ export type FeatureFault = {
|
|
|
217
147
|
message: string
|
|
218
148
|
}
|
|
219
149
|
|
|
220
|
-
/** Check a merged feature map against the SDK'S OWN DECLARATION.
|
|
221
|
-
*
|
|
222
|
-
* The schema arrives with the FACTORY rather than with a live client
|
|
223
|
-
* (§6.2), so this needs no construction and no network — which is what
|
|
224
|
-
* lets `check()` run it for every instance in CI.
|
|
225
|
-
*
|
|
226
|
-
* Derived from the descriptor, never hand-written, so it cannot drift:
|
|
227
|
-
* when a feature gains an option, the next regeneration teaches station
|
|
228
|
-
* about it with no station change.
|
|
229
|
-
*
|
|
230
|
-
* SCALARS AGREE BY CONSTRUCTION; COMPOUND OPTIONS ARE KIND-CHECKED, and
|
|
231
|
-
* that limit is real. An empty list default says nothing reliable about
|
|
232
|
-
* its element type and a nested map default says nothing about its
|
|
233
|
-
* value shapes, so `methods: [{}]` against a `['GET']` default is
|
|
234
|
-
* caught while `noProxy: []` accepts anything list-shaped. Closing that
|
|
235
|
-
* needs the feature model to declare an explicit option schema rather
|
|
236
|
-
* than only a default. */
|
|
237
150
|
export function checkfeatures(
|
|
238
151
|
merged: { [name: string]: any }, descriptor: any
|
|
239
152
|
): FeatureFault[] {
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
// @voxgig/station - one control surface for outbound integrations.
|
|
2
|
-
//
|
|
3
|
-
// THE ONLY ENTRY. There was a second, `index.browser.ts`, selected by a
|
|
4
|
-
// `browser` condition: the same surface minus the file-loading half, so
|
|
5
|
-
// no `node:` builtin was reachable from it. Station is server-side only,
|
|
6
|
-
// so that entry, the `ConfigFileIO` seam it needed and the graph-walk
|
|
7
|
-
// test that guarded it are all gone.
|
|
8
1
|
|
|
9
2
|
export { Station, instanceRef } from './Station'
|
|
10
3
|
export { provide, factoryFor, provided, resetFactories } from './factory'
|
package/src/loader.ts
CHANGED
|
@@ -1,43 +1,9 @@
|
|
|
1
|
-
/* The loader (design §6.3), where the language allows it.
|
|
2
|
-
*
|
|
3
|
-
* In ts, js, py, rb, php, perl, lua, elixir and clojure a module can be
|
|
4
|
-
* imported by name at runtime, so `api.<slug>.package` closes the loop:
|
|
5
|
-
* station imports the package (which triggers self-registration, §6.2
|
|
6
|
-
* path 1) and then looks up the factory.
|
|
7
|
-
*
|
|
8
|
-
* `sdk()` IS SYNCHRONOUS, AND IN TS/JS THAT BOUNDS WHAT THE LOADER CAN
|
|
9
|
-
* IMPORT. A CommonJS package loads synchronously through `require`; an
|
|
10
|
-
* ESM-only package only loads through `import()`, which is a promise.
|
|
11
|
-
* Making `sdk()` async to cover that would put an `await` in front of
|
|
12
|
-
* every call site in every language for a cost only two of them pay.
|
|
13
|
-
* So the rule is explicit rather than discovered at runtime:
|
|
14
|
-
*
|
|
15
|
-
* - the synchronous loader handles CommonJS, which is every SDK sdkgen
|
|
16
|
-
* generates (the ts target is `"type": "commonjs"`);
|
|
17
|
-
* - ESM packages load through an explicit `await station.load()`
|
|
18
|
-
* preload, after which `sdk()` is synchronous again for everything —
|
|
19
|
-
* one `await` at startup rather than one per call site;
|
|
20
|
-
* - a package only `import()` can load, reached through `sdk()`
|
|
21
|
-
* without a preload, is `station_sdk_load` WITH THE REMEDY IN THE
|
|
22
|
-
* MESSAGE. Never a silent half-loaded client.
|
|
23
|
-
*
|
|
24
|
-
* THIS IS A CODE-LOADING SURFACE DRIVEN BY A CONFIG FILE, so it has
|
|
25
|
-
* rules, and they are enforced here rather than documented and hoped
|
|
26
|
-
* for. See `checkPackage` and the `repoScoped` argument. */
|
|
27
1
|
|
|
28
2
|
import { StationError } from './error'
|
|
29
3
|
import { Factory, factoryFor, provide } from './factory'
|
|
30
4
|
|
|
31
|
-
/** The fixed alias every generated package exports.
|
|
32
|
-
*
|
|
33
|
-
* `export` defaults to this rather than to a derived class name because
|
|
34
|
-
* it is the same identifier in every generated package, where
|
|
35
|
-
* `camelify(slug) + 'SDK'` is a rule that has to be recomputed and can
|
|
36
|
-
* be wrong. The derived name is the SECOND attempt and an explicit
|
|
37
|
-
* `export` the third. */
|
|
38
5
|
export const DEFAULT_EXPORT = 'SDK'
|
|
39
6
|
|
|
40
|
-
/** `stripe-eu` -> `StripeEu`, for the second-attempt export name. */
|
|
41
7
|
export function camelify(slug: string): string {
|
|
42
8
|
return String(slug).split(/[^A-Za-z0-9]+/).filter((s) => '' !== s)
|
|
43
9
|
.map((s) => s[0].toUpperCase() + s.slice(1))
|
|
@@ -51,13 +17,6 @@ export function camelify(slug: string): string {
|
|
|
51
17
|
* name. */
|
|
52
18
|
export function checkPackage(api: string, pkg: string): string {
|
|
53
19
|
const p = String(pkg)
|
|
54
|
-
// A TRAVERSAL SEGMENT IS NOT A LEADING MARKER, and checking only the
|
|
55
|
-
// first character missed it: `pkg/../../escape` starts with neither
|
|
56
|
-
// `.` nor `/`, so it passed — and node resolves it through
|
|
57
|
-
// `node_modules/pkg/../../escape`, importing application-local code
|
|
58
|
-
// from outside the named dependency. The whole point of this function
|
|
59
|
-
// is that a configured package stays inside the dependency graph a
|
|
60
|
-
// reviewer can see.
|
|
61
20
|
const seg = p.split('/').some((x) => '.' === x || '..' === x)
|
|
62
21
|
const bad =
|
|
63
22
|
'' === p ||
|
|
@@ -75,10 +34,6 @@ export function checkPackage(api: string, pkg: string): string {
|
|
|
75
34
|
return p
|
|
76
35
|
}
|
|
77
36
|
|
|
78
|
-
/** Build a `{construct, config}` pair from a module that self-registered
|
|
79
|
-
* nothing — the retrofit path for a package whose SDK predates the
|
|
80
|
-
* station feature. It is not descriptor-blind: a generated main module
|
|
81
|
-
* exports its constructor AND the `config` singleton beside it. */
|
|
82
37
|
export function factoryFromModule(
|
|
83
38
|
api: string, mod: any, exportName?: string
|
|
84
39
|
): Factory {
|
|
@@ -124,8 +79,6 @@ export function loadSync(api: string, pkg: string, exportName?: string): boolean
|
|
|
124
79
|
mod = require(pkg)
|
|
125
80
|
}
|
|
126
81
|
catch (e: any) {
|
|
127
|
-
// ERR_REQUIRE_ESM is the case the preload exists for, and the
|
|
128
|
-
// message has to name the remedy rather than the symptom.
|
|
129
82
|
const code = String(e?.code || '')
|
|
130
83
|
if ('ERR_REQUIRE_ESM' === code) {
|
|
131
84
|
throw new StationError('station_sdk_load',
|
|
@@ -138,30 +91,12 @@ export function loadSync(api: string, pkg: string, exportName?: string): boolean
|
|
|
138
91
|
String(e?.message || e))
|
|
139
92
|
}
|
|
140
93
|
|
|
141
|
-
// Path 1: the module self-registered while being imported.
|
|
142
94
|
if (null != factoryFor(api)) { return true }
|
|
143
95
|
|
|
144
96
|
provide(api, factoryFromModule(api, mod, exportName))
|
|
145
97
|
return true
|
|
146
98
|
}
|
|
147
99
|
|
|
148
|
-
/** The ESM preload behind `await station.load()`. Imports the package
|
|
149
|
-
* and fills the SAME table the synchronous path fills. */
|
|
150
|
-
/** A dynamic `import()` that SURVIVES THE COMMONJS EMIT.
|
|
151
|
-
*
|
|
152
|
-
* This package compiles with `module: "commonjs"`, and TypeScript
|
|
153
|
-
* rewrites a literal `import(pkg)` into a promise around `require(pkg)`
|
|
154
|
-
* — so `await station.load()` threw `ERR_REQUIRE_ESM` for exactly the
|
|
155
|
-
* ESM-only packages it exists to preload, leaving no working path to
|
|
156
|
-
* those SDKs at all. Verified against the emitted `dist/src/loader.js`,
|
|
157
|
-
* which read `mod = require(pkg)`.
|
|
158
|
-
*
|
|
159
|
-
* `new Function` is the seam the compiler does not look inside. It is
|
|
160
|
-
* deliberately the only one in this file, and it takes no caller input:
|
|
161
|
-
* `pkg` has already been through `checkPackage`, and the body is a
|
|
162
|
-
* constant. Changing the package to `module: "node16"` would fix it
|
|
163
|
-
* more cleanly and is a whole-package decision rather than a defect
|
|
164
|
-
* fix. */
|
|
165
100
|
export const nativeImport: (p: string) => Promise<any> =
|
|
166
101
|
new Function('p', 'return import(p)') as any
|
|
167
102
|
|
|
@@ -181,7 +116,6 @@ export async function loadAsync(
|
|
|
181
116
|
|
|
182
117
|
if (null != factoryFor(api)) { return true }
|
|
183
118
|
|
|
184
|
-
// An ESM namespace puts a CommonJS module's exports under `default`.
|
|
185
119
|
const flat = null != mod?.default && null == mod?.config ? mod.default : mod
|
|
186
120
|
provide(api, factoryFromModule(api, flat, exportName))
|
|
187
121
|
return true
|