@larc-iu/plaid-client 0.1.0-alpha.8 → 0.2.0-alpha.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/index.d.ts +285 -105
- package/package.json +1 -1
- package/src/codepoint.js +73 -0
- package/src/http.js +36 -4
- package/src/index.js +501 -232
- package/src/provenance.js +123 -0
- package/src/roles.js +57 -0
- package/src/serviceSchema.js +212 -0
- package/src/services.js +21 -5
- package/src/sse.js +7 -0
- package/src/transforms.js +1 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provenance: the cross-app convention for distinguishing machine-provided
|
|
3
|
+
* from human-labeled information, expressed as flat metadata keys on
|
|
4
|
+
* annotation entities (spans, relations, vocab links, optionally tokens).
|
|
5
|
+
* Flat scalar keys — the query engine matches flat metadata well, nested
|
|
6
|
+
* objects poorly.
|
|
7
|
+
*
|
|
8
|
+
* Three states (the PRESENCE of the `prov` key, not its value, is the
|
|
9
|
+
* discriminator; absent keys mean "human"):
|
|
10
|
+
*
|
|
11
|
+
* human — no prov keys; a person made it.
|
|
12
|
+
* machine — { prov: 'inferred', provSource: '<producer>' };
|
|
13
|
+
* an algorithm/service made it, no human has vouched.
|
|
14
|
+
* verified — same + { provConfirmed: true }; machine-made and a
|
|
15
|
+
* human confirmed (or edited) it. provSource stays so
|
|
16
|
+
* machine origin remains traceable.
|
|
17
|
+
*
|
|
18
|
+
* The write contract every machine writer must follow:
|
|
19
|
+
* 1. Machine writers may freely replace MACHINE (unverified) material.
|
|
20
|
+
* 2. Machine writers must never modify or delete human or verified
|
|
21
|
+
* material unless explicitly told to overwrite (an explicit, per-run,
|
|
22
|
+
* user-facing opt-in — for services, a declared boolean `overwrite`
|
|
23
|
+
* parameter).
|
|
24
|
+
* 3. Any human edit of a machine annotation verifies it: the edit also
|
|
25
|
+
* stamps { provConfirmed: true } (see verifyOnEdit).
|
|
26
|
+
*
|
|
27
|
+
* Producer naming: 'service:<serviceId>' for services (use serviceSource),
|
|
28
|
+
* 'rule:<name>' for built-in rule algorithms, app-specific ids like
|
|
29
|
+
* 'gloss:doc-frequency' or 'flex-import' otherwise.
|
|
30
|
+
*
|
|
31
|
+
* PREDICTION EXTRAS. A producer may also record how confident it was and
|
|
32
|
+
* what else it considered, in two reserved slots split along the
|
|
33
|
+
* queryability line:
|
|
34
|
+
* - provProb: ONE flat number in [0, 1] — the producer's probability for
|
|
35
|
+
* the value it chose. Flat scalars are what the query engine filters
|
|
36
|
+
* and orders on, so "review the least-confident machine output first"
|
|
37
|
+
* is an ordinary query. Omit it unless you can honestly produce a
|
|
38
|
+
* probability (a raw logprob is NOT one — put it in provDetail).
|
|
39
|
+
* - provDetail: ONE open map for everything else — top-k alternatives or
|
|
40
|
+
* distributions, model name/version, raw scores. Deliberately nested
|
|
41
|
+
* (not queryable); keep it small (top-k, not whole-vocabulary dumps).
|
|
42
|
+
* Both describe the machine's ORIGINAL prediction. They are kept after
|
|
43
|
+
* human edits (history is valuable), so a consumer must not present
|
|
44
|
+
* provProb as confidence in the CURRENT value once the entity is verified
|
|
45
|
+
* (provConfirmed is exactly the flag to check).
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** The flat metadata keys of the provenance convention. */
|
|
49
|
+
export const PROV = Object.freeze({
|
|
50
|
+
/** 'inferred' when produced by an algorithm/service; absent = human. */
|
|
51
|
+
key: 'prov',
|
|
52
|
+
/** Which producer, e.g. 'service:<id>', 'rule:precedent-or-unique'. */
|
|
53
|
+
sourceKey: 'provSource',
|
|
54
|
+
/** true once a human confirmed (or edited) the inferred value. */
|
|
55
|
+
confirmedKey: 'provConfirmed',
|
|
56
|
+
/** Producer's probability ([0,1]) for the chosen value. Flat = queryable. */
|
|
57
|
+
probKey: 'provProb',
|
|
58
|
+
/** Open map of producer extras (alternatives, model version, raw scores). */
|
|
59
|
+
detailKey: 'provDetail',
|
|
60
|
+
/** The (currently only) value of `prov`. Presence, not value, decides. */
|
|
61
|
+
INFERRED: 'inferred',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** The three provenance states returned by provState. */
|
|
65
|
+
export const PROV_STATES = Object.freeze({
|
|
66
|
+
HUMAN: 'human',
|
|
67
|
+
MACHINE: 'machine',
|
|
68
|
+
VERIFIED: 'verified',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The metadata fragment a machine writer merges into everything it creates.
|
|
73
|
+
* @param {string} source - producer id, e.g. serviceSource(serviceId)
|
|
74
|
+
* @param {Object} [extras] - optional prediction extras:
|
|
75
|
+
* @param {number} [extras.prob] - probability in [0,1] for the chosen value
|
|
76
|
+
* (omit unless it honestly is one — raw scores go in detail)
|
|
77
|
+
* @param {Object} [extras.detail] - open map of producer extras (top-k
|
|
78
|
+
* alternatives, model version, raw scores; keep it small)
|
|
79
|
+
*/
|
|
80
|
+
export const stampInferred = (source, { prob, detail } = {}) => ({
|
|
81
|
+
[PROV.key]: PROV.INFERRED,
|
|
82
|
+
[PROV.sourceKey]: source,
|
|
83
|
+
...(prob !== undefined && prob !== null ? { [PROV.probKey]: prob } : {}),
|
|
84
|
+
...(detail ? { [PROV.detailKey]: detail } : {}),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The fragment for machine-made material that is born verified (e.g. an
|
|
89
|
+
* import carrying upstream human approval, or a guess written only on
|
|
90
|
+
* explicit user confirmation). Takes the same optional prediction extras
|
|
91
|
+
* as stampInferred.
|
|
92
|
+
*/
|
|
93
|
+
export const confirmedInferred = (source, extras = {}) => ({
|
|
94
|
+
...stampInferred(source, extras),
|
|
95
|
+
[PROV.confirmedKey]: true,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Classify an entity's metadata into one of PROV_STATES.
|
|
100
|
+
* @param {Object|null|undefined} metadata
|
|
101
|
+
* @returns {'human'|'machine'|'verified'}
|
|
102
|
+
*/
|
|
103
|
+
export const provState = (metadata) => {
|
|
104
|
+
if (!metadata || metadata[PROV.key] == null) return PROV_STATES.HUMAN;
|
|
105
|
+
return metadata[PROV.confirmedKey] ? PROV_STATES.VERIFIED : PROV_STATES.MACHINE;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Whether a machine writer must leave this entity alone (write-contract
|
|
110
|
+
* rule 2): true for human-made and human-verified material.
|
|
111
|
+
*/
|
|
112
|
+
export const isProtected = (metadata) => provState(metadata) !== PROV_STATES.MACHINE;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The metadata fragment a HUMAN edit of this entity should merge in
|
|
116
|
+
* (write-contract rule 3): { provConfirmed: true } when the entity is
|
|
117
|
+
* machine-made and unverified, else null (nothing to do).
|
|
118
|
+
*/
|
|
119
|
+
export const verifyOnEdit = (metadata) =>
|
|
120
|
+
(provState(metadata) === PROV_STATES.MACHINE ? { [PROV.confirmedKey]: true } : null);
|
|
121
|
+
|
|
122
|
+
/** Canonical provSource for a service: 'service:<serviceId>'. */
|
|
123
|
+
export const serviceSource = (serviceId) => `service:${serviceId}`;
|
package/src/roles.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared layer-role vocabulary for cross-app interoperability.
|
|
3
|
+
*
|
|
4
|
+
* Apps that share a Plaid project agree on the *substrate* — the text and token
|
|
5
|
+
* layers — by tagging each shared layer with a ROLE under the reserved `plaid`
|
|
6
|
+
* config namespace (`config.plaid.role`, a scalar). Annotations stay private to
|
|
7
|
+
* each app under that app's own namespace. See the Plaid manual, "Layer
|
|
8
|
+
* Interoperability". The role inventory is small and fixed:
|
|
9
|
+
*
|
|
10
|
+
* baseline the primary text layer
|
|
11
|
+
* sentence sentence token layer
|
|
12
|
+
* word orthographic-word token layer (CoNLL-U "token")
|
|
13
|
+
* syntactic-word grammatical words below the word (CoNLL-U "word" / MWT splits)
|
|
14
|
+
* morpheme morpheme token layer
|
|
15
|
+
* time-alignment media-timeline token layer
|
|
16
|
+
*
|
|
17
|
+
* Only these values are understood across apps; an app may store any string but
|
|
18
|
+
* loses interoperability for unknown values.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** The reserved config namespace for cross-app conventions. */
|
|
22
|
+
export const PLAID_NAMESPACE = 'plaid';
|
|
23
|
+
|
|
24
|
+
/** The config key, under `plaid`, holding a layer's role. */
|
|
25
|
+
export const ROLE_KEY = 'role';
|
|
26
|
+
|
|
27
|
+
/** The fixed role inventory. */
|
|
28
|
+
export const ROLES = Object.freeze({
|
|
29
|
+
BASELINE: 'baseline',
|
|
30
|
+
SENTENCE: 'sentence',
|
|
31
|
+
WORD: 'word',
|
|
32
|
+
SYNTACTIC_WORD: 'syntactic-word',
|
|
33
|
+
MORPHEME: 'morpheme',
|
|
34
|
+
TIME_ALIGNMENT: 'time-alignment',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The role recorded on a layer's `config`, or null if none.
|
|
39
|
+
* @param {object} [config] a layer's `config` object
|
|
40
|
+
* @returns {string|null}
|
|
41
|
+
*/
|
|
42
|
+
export function readRole(config) {
|
|
43
|
+
const v = config?.[PLAID_NAMESPACE]?.[ROLE_KEY];
|
|
44
|
+
return v == null ? null : v;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The first layer in `layers` carrying the given role, or null. The single
|
|
49
|
+
* "find a layer by its role" primitive — apps build their named finders
|
|
50
|
+
* (findWordTokenLayer, etc.) on top of this.
|
|
51
|
+
* @param {Array<{config?: object}>} [layers]
|
|
52
|
+
* @param {string} role
|
|
53
|
+
* @returns {object|null}
|
|
54
|
+
*/
|
|
55
|
+
export function findByRole(layers, role) {
|
|
56
|
+
return (layers || []).find(l => readRole(l?.config) === role) || null;
|
|
57
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service self-description: tasks, summary, and a parameter schema.
|
|
3
|
+
*
|
|
4
|
+
* The service framework's transport carries an opaque `extras` JSON map on every
|
|
5
|
+
* registered service (see services.js). This module standardizes what a service
|
|
6
|
+
* advertises in that map so apps can, at a fixed integration point (a "task"
|
|
7
|
+
* like tokenize / parse / transcribe):
|
|
8
|
+
*
|
|
9
|
+
* 1. SELECT one of several services that serve the task,
|
|
10
|
+
* 2. let the user SPECIFY arguments the service declares, and
|
|
11
|
+
* 3. show the user a service-provided SUMMARY.
|
|
12
|
+
*
|
|
13
|
+
* Shape of a service's `extras` (camelCase here; Python services author the
|
|
14
|
+
* snake_case equivalent and the client transform converts it — see the Plaid
|
|
15
|
+
* manual, "Describing a service"):
|
|
16
|
+
*
|
|
17
|
+
* {
|
|
18
|
+
* schemaVersion: 1,
|
|
19
|
+
* tasks: ["tokenize"], // controlled vocab; REPLACES tok:/asr: id prefixes
|
|
20
|
+
* summary: "## markdown …", // rich human description
|
|
21
|
+
* parameters: [ // ordered; rendered into a form
|
|
22
|
+
* { key, label, type, description?, default?, required?,
|
|
23
|
+
* options?: [{value, label}], // enum / multiselect
|
|
24
|
+
* min?, max?, step?, // number
|
|
25
|
+
* placeholder?, multiline? } // string
|
|
26
|
+
* ]
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* A parameter's `key` is a string VALUE, so it passes over the wire verbatim;
|
|
30
|
+
* the UI sends `{ [param.key]: value }` in the request data. Declare each `key`
|
|
31
|
+
* in your service's own convention (Python: snake_case; JS: camelCase) and read
|
|
32
|
+
* it back under that same key — request data is recased symmetrically, so a
|
|
33
|
+
* snake_case key round-trips unchanged to a JS UI and back.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The controlled task vocabulary — the fixed integration-point goals.
|
|
38
|
+
*
|
|
39
|
+
* Whatever the task, a service should stamp everything it creates with the
|
|
40
|
+
* provenance convention (see ./provenance.js: stampInferred(serviceSource(id)))
|
|
41
|
+
* so UIs can distinguish machine output until a human verifies it, and should
|
|
42
|
+
* honor the write contract: never destroy human-made or verified material
|
|
43
|
+
* without an explicit `overwrite` parameter. See the manual, "Provenance".
|
|
44
|
+
*/
|
|
45
|
+
export const TASKS = Object.freeze({
|
|
46
|
+
TOKENIZE: 'tokenize',
|
|
47
|
+
PARSE: 'parse',
|
|
48
|
+
TRANSCRIBE: 'transcribe',
|
|
49
|
+
/** Create vocab links for unlinked tokens. */
|
|
50
|
+
LINK_VOCAB: 'link-vocab',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Legacy id-prefix → task map, for services that have not yet migrated to a
|
|
55
|
+
* declared `tasks` array. Drop once all services advertise `tasks`.
|
|
56
|
+
*/
|
|
57
|
+
const LEGACY_TASK_PREFIXES = Object.freeze({
|
|
58
|
+
[TASKS.TOKENIZE]: 'tok:',
|
|
59
|
+
[TASKS.TRANSCRIBE]: 'asr:',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Does `service` serve `task`? Prefers the declared `extras.tasks` array; falls
|
|
64
|
+
* back to the legacy id-prefix convention for un-migrated services.
|
|
65
|
+
* @param {{serviceId?: string, extras?: {tasks?: string[]}}} service
|
|
66
|
+
* @param {string} task one of TASKS
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
export function servesTask(service, task) {
|
|
70
|
+
const declared = service?.extras?.tasks;
|
|
71
|
+
if (Array.isArray(declared) && declared.length) {
|
|
72
|
+
return declared.includes(task);
|
|
73
|
+
}
|
|
74
|
+
const prefix = LEGACY_TASK_PREFIXES[task];
|
|
75
|
+
return !!prefix && typeof service?.serviceId === 'string' && service.serviceId.startsWith(prefix);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The discovered services that serve `task`.
|
|
80
|
+
* @param {Array} services result of client.messages.discoverServices()
|
|
81
|
+
* @param {string} task one of TASKS
|
|
82
|
+
* @returns {Array}
|
|
83
|
+
*/
|
|
84
|
+
export function filterServicesByTask(services, task) {
|
|
85
|
+
return (services || []).filter((s) => servesTask(s, task));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The parameter schema a service declares (ordered array), or [].
|
|
90
|
+
* @param {{extras?: {parameters?: Array}}} service
|
|
91
|
+
* @returns {Array}
|
|
92
|
+
*/
|
|
93
|
+
export function getParamSchema(service) {
|
|
94
|
+
const params = service?.extras?.parameters;
|
|
95
|
+
return Array.isArray(params) ? params : [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A service's human summary: the rich `extras.summary`, else the short
|
|
100
|
+
* `description`, else ''.
|
|
101
|
+
* @param {{description?: string, extras?: {summary?: string}}} service
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
export function getServiceSummary(service) {
|
|
105
|
+
return service?.extras?.summary || service?.description || '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Valid option values for an enum/multiselect param. */
|
|
109
|
+
function optionValues(param) {
|
|
110
|
+
return Array.isArray(param?.options) ? param.options.map((o) => o.value) : [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A single parameter's default, honoring its declared `default` then falling
|
|
115
|
+
* back per type. For enum/multiselect the declared default is validated against
|
|
116
|
+
* `options` (an out-of-range declared default never escapes).
|
|
117
|
+
*/
|
|
118
|
+
function defaultForParam(param) {
|
|
119
|
+
const opts = optionValues(param);
|
|
120
|
+
if (param.type === 'enum') {
|
|
121
|
+
if (param.default != null && opts.includes(param.default)) return param.default;
|
|
122
|
+
return opts[0] ?? '';
|
|
123
|
+
}
|
|
124
|
+
if (param.type === 'multiselect') {
|
|
125
|
+
const arr = Array.isArray(param.default) ? param.default : [];
|
|
126
|
+
return opts.length ? arr.filter((x) => opts.includes(x)) : arr;
|
|
127
|
+
}
|
|
128
|
+
if (param.default !== undefined && param.default !== null) return param.default;
|
|
129
|
+
switch (param.type) {
|
|
130
|
+
case 'number': return typeof param.min === 'number' ? param.min : 0;
|
|
131
|
+
case 'boolean': return false;
|
|
132
|
+
case 'string':
|
|
133
|
+
default: return '';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Default values keyed by param key — the initial form state.
|
|
139
|
+
* @param {Array} schema getParamSchema(service)
|
|
140
|
+
* @returns {Object} { [key]: defaultValue }
|
|
141
|
+
*/
|
|
142
|
+
export function buildDefaultValues(schema) {
|
|
143
|
+
const out = {};
|
|
144
|
+
for (const param of schema || []) {
|
|
145
|
+
if (!param || !param.key) continue;
|
|
146
|
+
out[param.key] = defaultForParam(param);
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Coerce/validate raw form values against the schema. Returns the cleaned
|
|
153
|
+
* values (keyed by param key, ready to merge into the request payload) plus any
|
|
154
|
+
* validation errors keyed by param key. Unknown keys in `raw` are dropped.
|
|
155
|
+
* @param {Array} schema getParamSchema(service)
|
|
156
|
+
* @param {Object} raw current form values
|
|
157
|
+
* @returns {{values: Object, errors: Object}}
|
|
158
|
+
*/
|
|
159
|
+
export function coerceParamValues(schema, raw) {
|
|
160
|
+
const values = {};
|
|
161
|
+
const errors = {};
|
|
162
|
+
const src = raw || {};
|
|
163
|
+
for (const param of schema || []) {
|
|
164
|
+
if (!param || !param.key) continue;
|
|
165
|
+
const k = param.key;
|
|
166
|
+
let v = src[k];
|
|
167
|
+
if (v === undefined) v = defaultForParam(param);
|
|
168
|
+
|
|
169
|
+
switch (param.type) {
|
|
170
|
+
case 'number': {
|
|
171
|
+
// Blank / nullish counts as "missing" → the param's default (matches the
|
|
172
|
+
// Python client, where float('') raises and falls back to the default).
|
|
173
|
+
let n;
|
|
174
|
+
if (v === '' || v == null || (typeof v === 'string' && v.trim() === '')) {
|
|
175
|
+
n = defaultForParam(param);
|
|
176
|
+
} else {
|
|
177
|
+
n = typeof v === 'number' ? v : Number(v);
|
|
178
|
+
if (Number.isNaN(n)) n = defaultForParam(param);
|
|
179
|
+
}
|
|
180
|
+
if (typeof param.min === 'number') n = Math.max(param.min, n);
|
|
181
|
+
if (typeof param.max === 'number') n = Math.min(param.max, n);
|
|
182
|
+
v = n;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case 'boolean':
|
|
186
|
+
v = v === true || v === 'true';
|
|
187
|
+
break;
|
|
188
|
+
case 'enum': {
|
|
189
|
+
const opts = optionValues(param);
|
|
190
|
+
if (opts.length && !opts.includes(v)) v = defaultForParam(param);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case 'multiselect': {
|
|
194
|
+
const opts = optionValues(param);
|
|
195
|
+
const arr = Array.isArray(v) ? v : (v == null || v === '' ? [] : [v]);
|
|
196
|
+
v = opts.length ? arr.filter((x) => opts.includes(x)) : arr;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case 'string':
|
|
200
|
+
default:
|
|
201
|
+
v = v == null ? '' : String(v);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (param.required) {
|
|
206
|
+
const empty = v === '' || v == null || (Array.isArray(v) && v.length === 0);
|
|
207
|
+
if (empty) errors[k] = `${param.label || k} is required`;
|
|
208
|
+
}
|
|
209
|
+
values[k] = v;
|
|
210
|
+
}
|
|
211
|
+
return { values, errors };
|
|
212
|
+
}
|
package/src/services.js
CHANGED
|
@@ -11,18 +11,34 @@
|
|
|
11
11
|
import { transformRequest, transformResponse } from './transforms.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Discover the services
|
|
15
|
-
*
|
|
14
|
+
* Discover the services seen on a project — a synchronous GET. Returns every
|
|
15
|
+
* service ever registered on the project: currently connected ones carry
|
|
16
|
+
* `online: true`; previously-seen offline ones carry `online: false` plus a
|
|
17
|
+
* `lastSeenAt` stamp. Callers that need a service they can actually submit
|
|
18
|
+
* work to should filter on `online`.
|
|
16
19
|
*
|
|
17
20
|
* @param {Object} client - PlaidClient instance
|
|
18
21
|
* @param {string} projectId - Project UUID
|
|
19
|
-
* @
|
|
20
|
-
* @returns {Promise<Array>} [{serviceId, serviceName, description, extras}]
|
|
22
|
+
* @returns {Promise<Array>} [{serviceId, serviceName, description, extras, online, lastSeenAt}]
|
|
21
23
|
*/
|
|
22
|
-
export function discoverServices(client, projectId
|
|
24
|
+
export function discoverServices(client, projectId) {
|
|
23
25
|
return client._request('GET', `/api/v1/projects/${projectId}/services`);
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Forget a previously-seen (offline) service: removes its row from the
|
|
30
|
+
* project's persistent registry. Maintainer-only; 409 if the service is
|
|
31
|
+
* currently connected (it would just re-register).
|
|
32
|
+
*
|
|
33
|
+
* @param {Object} client - PlaidClient instance
|
|
34
|
+
* @param {string} projectId - Project UUID
|
|
35
|
+
* @param {string} serviceId - Service ID to forget
|
|
36
|
+
* @returns {Promise<void>}
|
|
37
|
+
*/
|
|
38
|
+
export function discardService(client, projectId, serviceId) {
|
|
39
|
+
return client._request('DELETE', `/api/v1/projects/${projectId}/services/${encodeURIComponent(serviceId)}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
/**
|
|
27
43
|
* Register a service and handle incoming work requests.
|
|
28
44
|
*
|
package/src/sse.js
CHANGED
|
@@ -75,6 +75,13 @@ export function createSSEConnection(client, projectId, onEvent, path) {
|
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
if (!response.ok) {
|
|
78
|
+
if (response.status === 409 && streamPath.includes('/services/')) {
|
|
79
|
+
// Service channel rejected: another live instance holds this
|
|
80
|
+
// service-id. The serve() reconnect timer will retry — once the
|
|
81
|
+
// other instance is gone (or its dead channel is reaped) the retry
|
|
82
|
+
// takes over.
|
|
83
|
+
console.warn('Service registration rejected (409): another instance of this service is already connected; will retry');
|
|
84
|
+
}
|
|
78
85
|
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
79
86
|
}
|
|
80
87
|
|
package/src/transforms.js
CHANGED
|
@@ -25,7 +25,7 @@ export function transformKeyFromCamel(key) {
|
|
|
25
25
|
// are never re-cased or namespace-stripped — a label like `case-marker` used as
|
|
26
26
|
// a map key survives intact. Everything else is API envelope and gets the
|
|
27
27
|
// usual case conversion.
|
|
28
|
-
const OPAQUE_KEYS = new Set(['metadata', 'config']);
|
|
28
|
+
const OPAQUE_KEYS = new Set(['metadata', 'config', 'bindings']);
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Recursively transform request object keys from camelCase to kebab-case.
|