@yuneta/gobj-ui 7.23.35 → 7.23.37
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/README.md +99 -10
- package/dist/gobj-ui.cjs.js +3325 -654
- package/dist/gobj-ui.es.js +3324 -655
- package/index.js +2 -0
- package/package.json +1 -1
- package/src/c_yui_fsm_graph.css +128 -0
- package/src/c_yui_fsm_graph.js +1015 -0
- package/src/c_yui_gclass.css +524 -0
- package/src/c_yui_gclass.js +1546 -0
- package/src/c_yui_gobj_tree_js.js +11 -27
- package/src/gclass_view_model.js +421 -0
- package/src/gclass_view_model.test.js +310 -0
- package/src/yui_gclass_view.js +42 -15
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import {describe, it, expect} from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
normalize_flags,
|
|
5
|
+
normalize_type,
|
|
6
|
+
normalize_default,
|
|
7
|
+
normalize_commands,
|
|
8
|
+
normalize_trace_levels,
|
|
9
|
+
build_fsm,
|
|
10
|
+
gclass_view_model,
|
|
11
|
+
} from "./gclass_view_model.js";
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* C_IEVENT_CLI as the C kernel answers it (`gclass2json()`), cut to
|
|
16
|
+
* what the model reads. Its FSM is the interesting one: three states,
|
|
17
|
+
* an output event handled nowhere, and ST_SESSION reached only from
|
|
18
|
+
* inside an action with gobj_change_state().
|
|
19
|
+
*/
|
|
20
|
+
const C_DIALECT = {
|
|
21
|
+
id: "C_IEVENT_CLI",
|
|
22
|
+
gcflag: [],
|
|
23
|
+
priv_size: 312,
|
|
24
|
+
attrs: [
|
|
25
|
+
{id: "url", type: "string", default_value: "", flag: "SDF_PERSIST",
|
|
26
|
+
description: "Url to connect"},
|
|
27
|
+
{id: "timeout_idack", type: "integer", default_value: "5000", flag: "SDF_RD",
|
|
28
|
+
description: "timeout waiting idAck"},
|
|
29
|
+
{id: "subscriber", type: "pointer", default_value: "", flag: "",
|
|
30
|
+
description: "subscriber of output-events"},
|
|
31
|
+
],
|
|
32
|
+
commands: [],
|
|
33
|
+
gclass_methods: ["mt_create", "mt_start", "mt_stop"],
|
|
34
|
+
internal_methods: [],
|
|
35
|
+
info_gclass_trace: {
|
|
36
|
+
"ievents": "Trace inter-events with metadata of kw",
|
|
37
|
+
"identity-card": "Trace identity_card messages",
|
|
38
|
+
},
|
|
39
|
+
instances: 4,
|
|
40
|
+
FSM: {
|
|
41
|
+
events: [
|
|
42
|
+
{event_name: "EV_ON_MESSAGE", event_flag: ""},
|
|
43
|
+
{event_name: "EV_ON_OPEN", event_flag: "EVF_OUTPUT_EVENT|EVF_NO_WARN_SUBS"},
|
|
44
|
+
{event_name: "EV_ON_CLOSE", event_flag: "EVF_OUTPUT_EVENT|EVF_NO_WARN_SUBS"},
|
|
45
|
+
{event_name: "EV_ON_ID_NAK", event_flag: "EVF_OUTPUT_EVENT"},
|
|
46
|
+
],
|
|
47
|
+
states: {
|
|
48
|
+
"ST_DISCONNECTED": [
|
|
49
|
+
{event_name: "EV_ON_OPEN", action: "action",
|
|
50
|
+
next_state: "ST_WAIT_IDENTITY_CARD_ACK"},
|
|
51
|
+
],
|
|
52
|
+
"ST_WAIT_IDENTITY_CARD_ACK": [
|
|
53
|
+
{event_name: "EV_ON_MESSAGE", action: "action", next_state: ""},
|
|
54
|
+
{event_name: "EV_ON_CLOSE", action: "action",
|
|
55
|
+
next_state: "ST_DISCONNECTED"},
|
|
56
|
+
],
|
|
57
|
+
"ST_SESSION": [
|
|
58
|
+
{event_name: "EV_ON_MESSAGE", action: "action", next_state: ""},
|
|
59
|
+
{event_name: "EV_ON_CLOSE", action: "action",
|
|
60
|
+
next_state: "ST_DISCONNECTED"},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/* The same gclass as the browser registry describes it. */
|
|
67
|
+
const JS_DIALECT = {
|
|
68
|
+
id: "C_YUI_JSON",
|
|
69
|
+
gcflag: ["gcflag_no_check_output_events"],
|
|
70
|
+
attrs: [
|
|
71
|
+
{id: "title", type: "DTP_STRING", flag: ["SDF_RD"], default_value: "",
|
|
72
|
+
description: "Optional header title"},
|
|
73
|
+
{id: "$container", type: "DTP_POINTER", flag: [], default_value: null,
|
|
74
|
+
description: "HTMLElement root"},
|
|
75
|
+
],
|
|
76
|
+
commands: [
|
|
77
|
+
{id: "help", alias: ["h"], description: "Command's help",
|
|
78
|
+
parameters: [
|
|
79
|
+
{id: "cmd", type: "DTP_STRING", flag: [], default_value: "",
|
|
80
|
+
description: "The command"},
|
|
81
|
+
]},
|
|
82
|
+
],
|
|
83
|
+
gclass_methods: ["mt_create", "mt_start"],
|
|
84
|
+
internal_methods: [],
|
|
85
|
+
trace_levels: [{name: "rows", description: "Trace rendered rows"}],
|
|
86
|
+
instances: 0,
|
|
87
|
+
FSM: {
|
|
88
|
+
events: [
|
|
89
|
+
{event_name: "EV_SET_JSON", event_flag: []},
|
|
90
|
+
{event_name: "EV_EXPAND_PATH",
|
|
91
|
+
event_flag: ["EVF_OUTPUT_EVENT", "EVF_NO_WARN_SUBS"]},
|
|
92
|
+
],
|
|
93
|
+
states: {
|
|
94
|
+
"ST_EMPTY": [
|
|
95
|
+
{event_name: "EV_SET_JSON", action: "ac_set_json",
|
|
96
|
+
next_state: "ST_READY"},
|
|
97
|
+
],
|
|
98
|
+
"ST_READY": [
|
|
99
|
+
{event_name: "EV_SET_JSON", action: "ac_set_json", next_state: ""},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
describe("normalize_flags", () => {
|
|
107
|
+
it("splits the C string form", () => {
|
|
108
|
+
expect(normalize_flags("SDF_WR|SDF_PERSIST")).toEqual(["SDF_WR", "SDF_PERSIST"]);
|
|
109
|
+
});
|
|
110
|
+
it("keeps the browser array form", () => {
|
|
111
|
+
expect(normalize_flags(["SDF_RD"])).toEqual(["SDF_RD"]);
|
|
112
|
+
});
|
|
113
|
+
it("answers a list for nothing at all", () => {
|
|
114
|
+
expect(normalize_flags("")).toEqual([]);
|
|
115
|
+
expect(normalize_flags(undefined)).toEqual([]);
|
|
116
|
+
expect(normalize_flags(0)).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("normalize_type", () => {
|
|
121
|
+
it("shortens the browser form", () => {
|
|
122
|
+
expect(normalize_type("DTP_STRING")).toBe("string");
|
|
123
|
+
});
|
|
124
|
+
it("lowers the C form", () => {
|
|
125
|
+
expect(normalize_type("integer")).toBe("integer");
|
|
126
|
+
});
|
|
127
|
+
it("reports an unknown type as what it is", () => {
|
|
128
|
+
expect(normalize_type(17)).toBe("17");
|
|
129
|
+
expect(normalize_type(undefined)).toBe("");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("normalize_default", () => {
|
|
134
|
+
it("spells out a null pointer instead of drawing an empty cell", () => {
|
|
135
|
+
expect(normalize_default(null)).toBe("null");
|
|
136
|
+
});
|
|
137
|
+
it("leaves a missing default empty", () => {
|
|
138
|
+
expect(normalize_default(undefined)).toBe("");
|
|
139
|
+
});
|
|
140
|
+
it("prints a json default", () => {
|
|
141
|
+
expect(normalize_default({a: 1})).toBe('{"a":1}');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("normalize_commands", () => {
|
|
146
|
+
it("reads the C command key and the browser id key", () => {
|
|
147
|
+
expect(normalize_commands([{command: "view-channels"}])[0].id)
|
|
148
|
+
.toBe("view-channels");
|
|
149
|
+
expect(normalize_commands([{id: "help"}])[0].id).toBe("help");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("turns a nameless C row into a section heading", () => {
|
|
153
|
+
const rows = normalize_commands([
|
|
154
|
+
{command: "help", description: "Command's help"},
|
|
155
|
+
{command: "", description: "\nOperation\n-----------"},
|
|
156
|
+
{command: "enable-channel", description: "Enable channel."},
|
|
157
|
+
]);
|
|
158
|
+
expect(rows.map((r) => r.kind))
|
|
159
|
+
.toEqual(["command", "section", "command"]);
|
|
160
|
+
expect(rows[1].label).toBe("Operation");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("drops a nameless row that says nothing", () => {
|
|
164
|
+
expect(normalize_commands([{command: "", description: ""}])).toEqual([]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("reads parameters from either key", () => {
|
|
168
|
+
const c = normalize_commands([
|
|
169
|
+
{command: "x", parameters: [{parameter: "channel_name", type: "string"}]},
|
|
170
|
+
]);
|
|
171
|
+
expect(c[0].parameters[0].id).toBe("channel_name");
|
|
172
|
+
const js = normalize_commands([
|
|
173
|
+
{id: "y", schema: [{id: "cmd", type: "DTP_STRING"}]},
|
|
174
|
+
]);
|
|
175
|
+
expect(js[0].parameters[0].id).toBe("cmd");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("normalize_trace_levels", () => {
|
|
180
|
+
it("reads the C dict", () => {
|
|
181
|
+
expect(normalize_trace_levels({info_gclass_trace: {a: "A"}}))
|
|
182
|
+
.toEqual([{id: "a", description: "A"}]);
|
|
183
|
+
});
|
|
184
|
+
it("reads the browser array", () => {
|
|
185
|
+
expect(normalize_trace_levels({trace_levels: [{name: "rows", description: "R"}]}))
|
|
186
|
+
.toEqual([{id: "rows", description: "R"}]);
|
|
187
|
+
});
|
|
188
|
+
it("answers a list when there are none", () => {
|
|
189
|
+
expect(normalize_trace_levels({})).toEqual([]);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("build_fsm", () => {
|
|
194
|
+
const fsm = build_fsm(C_DIALECT.FSM);
|
|
195
|
+
|
|
196
|
+
it("puts the events in the rows and the states in the columns", () => {
|
|
197
|
+
expect(fsm.states.map((s) => s.name)).toEqual([
|
|
198
|
+
"ST_DISCONNECTED", "ST_WAIT_IDENTITY_CARD_ACK", "ST_SESSION",
|
|
199
|
+
]);
|
|
200
|
+
expect(fsm.rows.map((r) => r.event)).toEqual([
|
|
201
|
+
"EV_ON_MESSAGE", "EV_ON_OPEN", "EV_ON_CLOSE",
|
|
202
|
+
]);
|
|
203
|
+
expect(fsm.rows[0].cells.length).toBe(3);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("leaves a cell null where the state declares nothing", () => {
|
|
207
|
+
const on_open = fsm.rows.find((r) => r.event === "EV_ON_OPEN");
|
|
208
|
+
expect(on_open.cells[0].next_state).toBe("ST_WAIT_IDENTITY_CARD_ACK");
|
|
209
|
+
expect(on_open.cells[1]).toBe(null);
|
|
210
|
+
expect(on_open.cells[2]).toBe(null);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("keeps an event out of the matrix when no state handles it", () => {
|
|
214
|
+
expect(fsm.published.map((p) => p.event)).toEqual(["EV_ON_ID_NAK"]);
|
|
215
|
+
expect(fsm.published[0].flags).toEqual(["EVF_OUTPUT_EVENT"]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("tells a declared-but-actionless cell from an action", () => {
|
|
219
|
+
/* `{EV_TX_READY, 0, 0}` -- four of them in C_WEBSOCKET: the
|
|
220
|
+
* event is legal in that state and deliberately does nothing. */
|
|
221
|
+
const none = build_fsm({
|
|
222
|
+
events: [{event_name: "EV_TX_READY", event_flag: ""}],
|
|
223
|
+
states: {"ST_IDLE": [
|
|
224
|
+
{event_name: "EV_TX_READY", action: "", next_state: ""},
|
|
225
|
+
]},
|
|
226
|
+
});
|
|
227
|
+
expect(none.rows[0].cells[0]).toEqual({
|
|
228
|
+
action: "", has_action: false, next_state: "",
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("says an action exists without inventing its name", () => {
|
|
233
|
+
expect(fsm.rows[0].cells[1]).toEqual({
|
|
234
|
+
action: "", has_action: true, next_state: "",
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("keeps the action name when the browser gives one", () => {
|
|
239
|
+
const js = build_fsm(JS_DIALECT.FSM);
|
|
240
|
+
expect(js.rows[0].cells[0].action).toBe("ac_set_json");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("marks a state nothing declares a way into", () => {
|
|
244
|
+
expect(fsm.unreachable).toEqual(["ST_SESSION"]);
|
|
245
|
+
expect(fsm.entry_state).toBe("ST_DISCONNECTED");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("never marks the entry state", () => {
|
|
249
|
+
const one = build_fsm({events: [], states: {"ST_IDLE": []}});
|
|
250
|
+
expect(one.unreachable).toEqual([]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("counts what each state handles", () => {
|
|
254
|
+
expect(fsm.states.map((s) => s.count)).toEqual([1, 2, 2]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("shows an event a state handles but the table forgot to declare", () => {
|
|
258
|
+
const orphan = build_fsm({
|
|
259
|
+
events: [],
|
|
260
|
+
states: {"ST_IDLE": [{event_name: "EV_GHOST", action: "ac_x", next_state: ""}]},
|
|
261
|
+
});
|
|
262
|
+
expect(orphan.rows.map((r) => r.event)).toEqual(["EV_GHOST"]);
|
|
263
|
+
expect(orphan.rows[0].flags).toEqual([]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("survives a gclass with no machine at all", () => {
|
|
267
|
+
const none = build_fsm(undefined);
|
|
268
|
+
expect(none.states).toEqual([]);
|
|
269
|
+
expect(none.rows).toEqual([]);
|
|
270
|
+
expect(none.published).toEqual([]);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("gclass_view_model", () => {
|
|
275
|
+
it("reads the C dialect whole", () => {
|
|
276
|
+
const m = gclass_view_model(C_DIALECT);
|
|
277
|
+
expect(m.id).toBe("C_IEVENT_CLI");
|
|
278
|
+
expect(m.instances).toBe(4);
|
|
279
|
+
expect(m.priv_size).toBe(312);
|
|
280
|
+
expect(m.attrs[0]).toEqual({
|
|
281
|
+
id: "url", type: "string", flags: ["SDF_PERSIST"],
|
|
282
|
+
default_value: "", description: "Url to connect",
|
|
283
|
+
});
|
|
284
|
+
expect(m.trace_levels.map((r) => r.id)).toEqual(["ievents", "identity-card"]);
|
|
285
|
+
expect(m.commands).toEqual([]);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("reads the browser dialect whole", () => {
|
|
289
|
+
const m = gclass_view_model(JS_DIALECT);
|
|
290
|
+
expect(m.gcflag).toEqual(["gcflag_no_check_output_events"]);
|
|
291
|
+
expect(m.attrs[1].default_value).toBe("null");
|
|
292
|
+
expect(m.commands[0].parameters[0].id).toBe("cmd");
|
|
293
|
+
expect(m.methods).toEqual(["mt_create", "mt_start"]);
|
|
294
|
+
expect(m.instances).toBe(0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("answers null for nothing", () => {
|
|
298
|
+
expect(gclass_view_model(null)).toBe(null);
|
|
299
|
+
expect(gclass_view_model("C_TIMER")).toBe(null);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("draws empty zones for a description missing keys", () => {
|
|
303
|
+
const m = gclass_view_model({id: "C_BARE"});
|
|
304
|
+
expect(m.attrs).toEqual([]);
|
|
305
|
+
expect(m.commands).toEqual([]);
|
|
306
|
+
expect(m.trace_levels).toEqual([]);
|
|
307
|
+
expect(m.instances).toBe(null);
|
|
308
|
+
expect(m.fsm.rows).toEqual([]);
|
|
309
|
+
});
|
|
310
|
+
});
|
package/src/yui_gclass_view.js
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* The framework had no such viewer. The C kernel answers
|
|
7
7
|
* `view-gclass` with a full description (attrs, commands,
|
|
8
8
|
* methods, FSM) and the only reader of it was a terminal.
|
|
9
|
-
* This one draws that same document with
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* This one draws that same document with C_YUI_GCLASS, which
|
|
10
|
+
* lays it out by zones -- attributes, commands, methods,
|
|
11
|
+
* trace levels, and the machine as a matrix -- and keeps the
|
|
12
|
+
* raw answer one button away. It takes the document from
|
|
13
|
+
* either side:
|
|
12
14
|
*
|
|
13
15
|
* - a gclass of the browser yuno -> describe_local_gclass()
|
|
14
16
|
* - a gclass of a backend yuno -> `opts.description`, the
|
|
@@ -39,23 +41,38 @@ import {
|
|
|
39
41
|
} from "@yuneta/gobj-js";
|
|
40
42
|
|
|
41
43
|
import {describe_local_gclass} from "./gclass_describe.js";
|
|
44
|
+
import {register_c_yui_gclass} from "./c_yui_gclass.js";
|
|
42
45
|
import {yui_shell_show_error, yui_shell_show_modal, yui_shell_popup_layer} from "./shell_modals.js";
|
|
43
46
|
import {yui_shell_of} from "./c_yui_shell.js";
|
|
44
47
|
|
|
45
48
|
import {t} from "i18next";
|
|
46
49
|
|
|
47
50
|
|
|
51
|
+
/*
|
|
52
|
+
* The gclass that draws a gclass. It is registered on demand here
|
|
53
|
+
* (see gclass_view_available), so a host offering the control does
|
|
54
|
+
* not have to mount it.
|
|
55
|
+
*/
|
|
56
|
+
const VIEWER_GCLASS = "C_YUI_GCLASS";
|
|
57
|
+
|
|
58
|
+
|
|
48
59
|
/************************************************************
|
|
49
60
|
* Can this app show a gclass at all?
|
|
50
61
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
62
|
+
* Always: the viewer registers ITSELF here, so no app has to
|
|
63
|
+
* remember to mount a gclass for a control it never asked
|
|
64
|
+
* for. The predicate stays because every caller asks it
|
|
65
|
+
* BEFORE drawing the button, and because registration can
|
|
66
|
+
* still fail -- a gclass_create() that returns -1 leaves the
|
|
67
|
+
* registry without it, and a button that opens nothing is
|
|
68
|
+
* worse than no button.
|
|
55
69
|
************************************************************/
|
|
56
70
|
function gclass_view_available()
|
|
57
71
|
{
|
|
58
|
-
|
|
72
|
+
if(!gclass_find_by_name(VIEWER_GCLASS, false)) {
|
|
73
|
+
register_c_yui_gclass();
|
|
74
|
+
}
|
|
75
|
+
return !!gclass_find_by_name(VIEWER_GCLASS, false);
|
|
59
76
|
}
|
|
60
77
|
|
|
61
78
|
|
|
@@ -65,6 +82,9 @@ function gclass_view_available()
|
|
|
65
82
|
* opts:
|
|
66
83
|
* description the document to draw. Omitted, it is
|
|
67
84
|
* built from the browser registry.
|
|
85
|
+
* current_state the state the INSTANCE the reader came
|
|
86
|
+
* from is in, lit in the machine. Omitted,
|
|
87
|
+
* nothing is lit.
|
|
68
88
|
* title_prefix what the window says before the gclass
|
|
69
89
|
* name (the yuno it belongs to).
|
|
70
90
|
* on_close called when the reader dismisses it.
|
|
@@ -84,7 +104,7 @@ function open_gclass_view(host, gclass_name, opts)
|
|
|
84
104
|
let shell = yui_shell_of(host);
|
|
85
105
|
|
|
86
106
|
if(!gclass_view_available()) {
|
|
87
|
-
log_error(`${gobj_short_name(host)}:
|
|
107
|
+
log_error(`${gobj_short_name(host)}: cannot register ${VIEWER_GCLASS}`);
|
|
88
108
|
yui_shell_show_error(shell, "gclass viewer unavailable", {t: t});
|
|
89
109
|
return null;
|
|
90
110
|
}
|
|
@@ -101,13 +121,20 @@ function open_gclass_view(host, gclass_name, opts)
|
|
|
101
121
|
|
|
102
122
|
let jv = gobj_create_service(
|
|
103
123
|
`gclass-view-${clean_name(gobj_name(host))}`,
|
|
104
|
-
|
|
124
|
+
VIEWER_GCLASS,
|
|
105
125
|
{
|
|
106
|
-
/* No
|
|
107
|
-
* on desktop, the dialog's header on mobile. The
|
|
108
|
-
* own title would land INSIDE that host,
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
/* No title attr: the host titles it -- the window's title
|
|
127
|
+
* bar on desktop, the dialog's header on mobile. The
|
|
128
|
+
* viewer's own title would land INSIDE that host,
|
|
129
|
+
* doubling it. */
|
|
130
|
+
description: description,
|
|
131
|
+
gclass_name: gclass_name,
|
|
132
|
+
/* Which state the INSTANCE the reader came from is in.
|
|
133
|
+
* The description cannot carry it -- it describes the
|
|
134
|
+
* CLASS -- and the caller is the only party that knows,
|
|
135
|
+
* so an omitted one simply lights nothing. */
|
|
136
|
+
current_state: opts.current_state || "",
|
|
137
|
+
subscriber: null,
|
|
111
138
|
},
|
|
112
139
|
host
|
|
113
140
|
);
|