@qbix/q 1.0.5
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/LICENSE +21 -0
- package/README.md +678 -0
- package/dist/Metrics.js +2873 -0
- package/dist/Metrics.min.js +95 -0
- package/dist/Q.js +15677 -0
- package/dist/Q.min.js +385 -0
- package/dist/Q.minimal.js +11594 -0
- package/dist/Q.minimal.min.js +286 -0
- package/dist/handlebars-v4.0.10.min.js +29 -0
- package/dist/handlebars.minimal.min.js +1 -0
- package/dist/img/hints/rotate-left.gif +0 -0
- package/dist/img/hints/swipe-down.gif +0 -0
- package/dist/img/hints/swipe-up.gif +0 -0
- package/dist/img/hints/tap.gif +0 -0
- package/dist/img/throbbers/loading.gif +0 -0
- package/dist/jquery.minimal.min.js +18 -0
- package/dist/methods/Q/Audio/load.js +29 -0
- package/dist/methods/Q/Audio/loadVoices.js +36 -0
- package/dist/methods/Q/Audio/play.js +47 -0
- package/dist/methods/Q/Audio/speak.js +149 -0
- package/dist/methods/Q/Crypto/delegate.js +186 -0
- package/dist/methods/Q/Crypto/internalKeypair.js +170 -0
- package/dist/methods/Q/Crypto/sign.js +200 -0
- package/dist/methods/Q/Crypto/verify.js +212 -0
- package/dist/methods/Q/Crypto/verifyDelegated.js +214 -0
- package/dist/methods/Q/Data/Bloom/_internal.js +163 -0
- package/dist/methods/Q/Data/Bloom/create.js +23 -0
- package/dist/methods/Q/Data/Bloom/fromBase64.js +21 -0
- package/dist/methods/Q/Data/Bloom/fromBytes.js +15 -0
- package/dist/methods/Q/Data/Bloom/fromElements.js +33 -0
- package/dist/methods/Q/Data/Merkle/_internal.js +68 -0
- package/dist/methods/Q/Data/Merkle/build.js +29 -0
- package/dist/methods/Q/Data/Merkle/proof.js +50 -0
- package/dist/methods/Q/Data/Merkle/verify.js +32 -0
- package/dist/methods/Q/Data/Prolly/_internal.js +190 -0
- package/dist/methods/Q/Data/Prolly/build.js +28 -0
- package/dist/methods/Q/Data/Prolly/delete.js +28 -0
- package/dist/methods/Q/Data/Prolly/diff.js +70 -0
- package/dist/methods/Q/Data/Prolly/get.js +38 -0
- package/dist/methods/Q/Data/Prolly/set.js +32 -0
- package/dist/methods/Q/Data/compress.js +45 -0
- package/dist/methods/Q/Data/decompress.js +35 -0
- package/dist/methods/Q/Data/decrypt.js +55 -0
- package/dist/methods/Q/Data/derive.js +76 -0
- package/dist/methods/Q/Data/digest.js +29 -0
- package/dist/methods/Q/Data/encrypt.js +61 -0
- package/dist/methods/Q/Data/generateKey.js +40 -0
- package/dist/methods/Q/Data/hkdf.js +44 -0
- package/dist/methods/Q/Data/importKey.js +34 -0
- package/dist/methods/Q/Data/sign.js +42 -0
- package/dist/methods/Q/Data/verify.js +45 -0
- package/dist/methods/Q/Onboarding/handle.js +50 -0
- package/dist/methods/Q/Onboarding/start.js +165 -0
- package/dist/methods/Q/Onboarding/stop.js +21 -0
- package/dist/methods/Q/Sandbox/run.js +392 -0
- package/dist/methods/Q/Tool/define/component.js +218 -0
- package/dist/methods/Q/globalMemoryWalk.js +82 -0
- package/dist/methods/Q/leaves.js +34 -0
- package/dist/methods/Q/registerWebComponent.js +0 -0
- package/dist/methods/Q/sanitize.js +142 -0
- package/dist/test.html +6 -0
- package/dist/tools/Q/lazyload.js +433 -0
- package/package.json +26 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
Q.exports(function (Q) {
|
|
2
|
+
/**
|
|
3
|
+
* Q plugin's front-end code
|
|
4
|
+
* @module Q
|
|
5
|
+
* @class Q.Sandbox
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Runs code safely inside a sandboxed Web Worker.
|
|
10
|
+
* If `options.name` is provided, a persistent worker is reused.
|
|
11
|
+
*
|
|
12
|
+
* @static
|
|
13
|
+
* @method run
|
|
14
|
+
* @param {String} code JavaScript source to execute
|
|
15
|
+
* @param {Object} [context] Variables accessible inside the sandbox
|
|
16
|
+
* @param {Object} [methods] Async RPC methods exposed as stubs
|
|
17
|
+
* @param {Object} [options] Additional sandbox configuration
|
|
18
|
+
* @param {String} [options.name] Reuse a persistent sandbox worker under this name
|
|
19
|
+
* @param {Number} [options.timeout=2000] Timeout in milliseconds before aborting execution
|
|
20
|
+
* @param {Boolean} [options.db=false] Whether to expose indexedDB inside sandbox
|
|
21
|
+
* @param {Boolean|Object} [options.deterministic=false] Set to true or object to make the code run deterministically
|
|
22
|
+
* @param {Number} [options.deterministic.seed=1] Seed for deterministic RNG
|
|
23
|
+
* @return {Q.Promise} Resolves with result or rejects on error
|
|
24
|
+
*/
|
|
25
|
+
return function Q_Sandbox_run(code, context, methods, options) {
|
|
26
|
+
context = context || {};
|
|
27
|
+
methods = methods || {};
|
|
28
|
+
options = options || {};
|
|
29
|
+
|
|
30
|
+
if (!Q.Sandbox._runners) Q.Sandbox._runners = {};
|
|
31
|
+
|
|
32
|
+
function SandboxRunner(defaults) {
|
|
33
|
+
this.defaults = {
|
|
34
|
+
timeout: (defaults && defaults.timeout) || 2000,
|
|
35
|
+
db: !!(defaults && defaults.db)
|
|
36
|
+
};
|
|
37
|
+
this.worker = null;
|
|
38
|
+
this.url = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
SandboxRunner.prototype.createWorker = function () {
|
|
42
|
+
const allowDB = !!this.defaults.db;
|
|
43
|
+
const indexedDBExpr = allowDB ? 'indexedDB' : 'undefined';
|
|
44
|
+
|
|
45
|
+
const script = `
|
|
46
|
+
// --- Hard-disable network & import capabilities ---
|
|
47
|
+
self.fetch = undefined;
|
|
48
|
+
self.XMLHttpRequest = undefined;
|
|
49
|
+
self.WebSocket = undefined;
|
|
50
|
+
self.EventSource = undefined;
|
|
51
|
+
self.importScripts = undefined;
|
|
52
|
+
|
|
53
|
+
// --- Safe stubs instead of deleting env ---
|
|
54
|
+
try {
|
|
55
|
+
Object.defineProperty(self, "navigator", {
|
|
56
|
+
value: { userAgent: "sandbox", language: "en-US" },
|
|
57
|
+
configurable: false
|
|
58
|
+
});
|
|
59
|
+
} catch {}
|
|
60
|
+
|
|
61
|
+
self.location = undefined;
|
|
62
|
+
self.caches = undefined;
|
|
63
|
+
|
|
64
|
+
// Optional DB
|
|
65
|
+
if (!${allowDB}) {
|
|
66
|
+
self.indexedDB = undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Block prototype mutation entry points (Safari-safe) ---
|
|
70
|
+
try {
|
|
71
|
+
Object.defineProperty(Object.prototype, "__defineSetter__", { value: undefined });
|
|
72
|
+
Object.defineProperty(Object.prototype, "__defineGetter__", { value: undefined });
|
|
73
|
+
Object.defineProperty(Object.prototype, "__lookupGetter__", { value: undefined });
|
|
74
|
+
Object.defineProperty(Object.prototype, "__lookupSetter__", { value: undefined });
|
|
75
|
+
} catch {}
|
|
76
|
+
|
|
77
|
+
let rpcCounter = 0;
|
|
78
|
+
const pending = {};
|
|
79
|
+
|
|
80
|
+
function call(method, args) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const id = ++rpcCounter;
|
|
83
|
+
pending[id] = { resolve, reject };
|
|
84
|
+
self.postMessage({ type: "rpc", id, method, args });
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
self.onmessage = async function (e) {
|
|
89
|
+
const msg = e.data;
|
|
90
|
+
|
|
91
|
+
if (msg && msg.type === "rpcResult") {
|
|
92
|
+
const p = pending[msg.id];
|
|
93
|
+
if (!p) return;
|
|
94
|
+
delete pending[msg.id];
|
|
95
|
+
msg.ok ? p.resolve(msg.result) : p.reject(msg.error);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const { code, context, methodNames, deterministic } = msg;
|
|
101
|
+
|
|
102
|
+
let __seed = 1;
|
|
103
|
+
if (deterministic && typeof deterministic === "object" && deterministic.seed !== undefined) {
|
|
104
|
+
__seed = deterministic.seed >>> 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const __timers = [];
|
|
108
|
+
let __timerGuard = 1000;
|
|
109
|
+
|
|
110
|
+
// --- Deterministic runtime injected only if requested ---
|
|
111
|
+
if (deterministic) {
|
|
112
|
+
let __randSeed = (__seed >>> 0) || 1;
|
|
113
|
+
|
|
114
|
+
function __rand(){
|
|
115
|
+
__randSeed = (__randSeed * 1664525 + 1013904223) >>> 0;
|
|
116
|
+
return __randSeed / 4294967296;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Object.defineProperty(self, "__deterministicSeed", {
|
|
120
|
+
value: __randSeed,
|
|
121
|
+
writable: false,
|
|
122
|
+
configurable: false
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
Math.random = __rand;
|
|
126
|
+
Object.defineProperty(Math, "random", {
|
|
127
|
+
value: __rand,
|
|
128
|
+
writable: false,
|
|
129
|
+
configurable: false
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const __start = 0;
|
|
133
|
+
|
|
134
|
+
Date.now = function(){ return __start };
|
|
135
|
+
|
|
136
|
+
if (typeof performance !== "undefined") {
|
|
137
|
+
performance.now = function(){ return 0 };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const __RealDate = Date;
|
|
141
|
+
|
|
142
|
+
function DeterministicDate(...args) {
|
|
143
|
+
if (!(this instanceof DeterministicDate)) {
|
|
144
|
+
return new __RealDate(__start).toString();
|
|
145
|
+
}
|
|
146
|
+
if (args.length === 0) {
|
|
147
|
+
return new __RealDate(__start);
|
|
148
|
+
}
|
|
149
|
+
return new __RealDate(...args);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
DeterministicDate.UTC = __RealDate.UTC;
|
|
153
|
+
DeterministicDate.parse = __RealDate.parse;
|
|
154
|
+
DeterministicDate.prototype = __RealDate.prototype;
|
|
155
|
+
DeterministicDate.prototype.constructor = DeterministicDate;
|
|
156
|
+
|
|
157
|
+
Date = DeterministicDate;
|
|
158
|
+
|
|
159
|
+
setTimeout = function(fn){ __timers.push(fn); return __timers.length };
|
|
160
|
+
setInterval = function(fn){ __timers.push(fn); return __timers.length };
|
|
161
|
+
|
|
162
|
+
clearTimeout = function(){};
|
|
163
|
+
clearInterval = function(){};
|
|
164
|
+
|
|
165
|
+
if (typeof crypto !== "undefined") {
|
|
166
|
+
crypto.getRandomValues = function(arr){
|
|
167
|
+
for (let i=0;i<arr.length;i++){
|
|
168
|
+
arr[i] = Math.floor(__rand()*256);
|
|
169
|
+
}
|
|
170
|
+
return arr;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
crypto.randomUUID = function(){
|
|
174
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,function(c){
|
|
175
|
+
const r = Math.floor(__rand()*16);
|
|
176
|
+
return (c==='x'?r:(r&0x3|0x8)).toString(16);
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
self.fetch = undefined;
|
|
182
|
+
self.XMLHttpRequest = undefined;
|
|
183
|
+
self.WebSocket = undefined;
|
|
184
|
+
self.EventSource = undefined;
|
|
185
|
+
self.navigator = undefined;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
Object.freeze(Math);
|
|
189
|
+
Object.freeze(Date);
|
|
190
|
+
} catch {}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const methods = {};
|
|
194
|
+
for (const name of methodNames) {
|
|
195
|
+
methods[name] = (...args) => call(name, args);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const keys = Object.keys(context || {}).concat("methods");
|
|
199
|
+
const values = Object.values(context || {}).concat(methods);
|
|
200
|
+
|
|
201
|
+
const AsyncFunction =
|
|
202
|
+
Object.getPrototypeOf(async function () {}).constructor;
|
|
203
|
+
|
|
204
|
+
const fn = new AsyncFunction(
|
|
205
|
+
...keys,
|
|
206
|
+
'"use strict";\\n' +
|
|
207
|
+
'const fetch = undefined;\\n' +
|
|
208
|
+
'const XMLHttpRequest = undefined;\\n' +
|
|
209
|
+
'const WebSocket = undefined;\\n' +
|
|
210
|
+
'const EventSource = undefined;\\n' +
|
|
211
|
+
'const importScripts = undefined;\\n' +
|
|
212
|
+
'const indexedDB = ${allowDB ? 'self.indexedDB' : 'undefined'};\\n' +
|
|
213
|
+
'const IDBFactory = undefined;\\n' +
|
|
214
|
+
'const IDBDatabase = undefined;\\n' +
|
|
215
|
+
'const IDBObjectStore = undefined;\\n' +
|
|
216
|
+
'const __user = async function(){\\n' +
|
|
217
|
+
code + '\\n' +
|
|
218
|
+
'};\\n' +
|
|
219
|
+
'return __user();'
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const result = await fn(...values);
|
|
223
|
+
|
|
224
|
+
// run deterministic timers
|
|
225
|
+
while (__timers.length && __timerGuard--) {
|
|
226
|
+
try { __timers.shift()(); } catch {}
|
|
227
|
+
}
|
|
228
|
+
__timers.length = 0;
|
|
229
|
+
|
|
230
|
+
self.postMessage({ type: "done", ok: true, result });
|
|
231
|
+
|
|
232
|
+
} catch (err) {
|
|
233
|
+
self.postMessage({
|
|
234
|
+
type: "done",
|
|
235
|
+
ok: false,
|
|
236
|
+
error: String(err && err.message || err)
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
`;
|
|
241
|
+
|
|
242
|
+
const blob = new Blob([script], { type: "application/javascript" });
|
|
243
|
+
this.url = URL.createObjectURL(blob);
|
|
244
|
+
this.worker = new Worker(this.url);
|
|
245
|
+
return this.worker;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
SandboxRunner.prototype.run = function (code, ctx, methods, opts) {
|
|
249
|
+
opts = opts || {};
|
|
250
|
+
const worker = this.worker || this.createWorker();
|
|
251
|
+
const timeoutMs = opts.timeout || this.defaults.timeout;
|
|
252
|
+
|
|
253
|
+
let safeCtx;
|
|
254
|
+
try {
|
|
255
|
+
safeCtx = JSON.parse(JSON.stringify(ctx));
|
|
256
|
+
} catch {
|
|
257
|
+
safeCtx = {};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const methodNames = Object.keys(methods);
|
|
261
|
+
|
|
262
|
+
return new Q.Promise(function (resolve, reject) {
|
|
263
|
+
let timer;
|
|
264
|
+
|
|
265
|
+
const runner = this;
|
|
266
|
+
const cleanup = () => {
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
if (!opts.name) {
|
|
269
|
+
try {
|
|
270
|
+
URL.revokeObjectURL(runner.url);
|
|
271
|
+
worker.terminate();
|
|
272
|
+
} catch {}
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const rpcLog = [];
|
|
277
|
+
let finished = false;
|
|
278
|
+
|
|
279
|
+
worker.onmessage = function (e) {
|
|
280
|
+
const msg = e.data;
|
|
281
|
+
|
|
282
|
+
if (msg && msg.type === "rpc") {
|
|
283
|
+
const fn = methods[msg.method];
|
|
284
|
+
if (!fn) {
|
|
285
|
+
worker.postMessage({
|
|
286
|
+
type: "rpcResult",
|
|
287
|
+
id: msg.id,
|
|
288
|
+
ok: false,
|
|
289
|
+
error: "Unknown method: " + msg.method
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
Promise.resolve()
|
|
295
|
+
.then(() => fn(...msg.args))
|
|
296
|
+
.then(result => {
|
|
297
|
+
rpcLog.push({
|
|
298
|
+
method: msg.method,
|
|
299
|
+
args: msg.args,
|
|
300
|
+
result
|
|
301
|
+
});
|
|
302
|
+
worker.postMessage({
|
|
303
|
+
type: "rpcResult",
|
|
304
|
+
id: msg.id,
|
|
305
|
+
ok: true,
|
|
306
|
+
result
|
|
307
|
+
});
|
|
308
|
+
})
|
|
309
|
+
.catch(err => {
|
|
310
|
+
rpcLog.push({
|
|
311
|
+
method: msg.method,
|
|
312
|
+
args: msg.args,
|
|
313
|
+
error: String(err && err.message || err)
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
worker.postMessage({
|
|
317
|
+
type: "rpcResult",
|
|
318
|
+
id: msg.id,
|
|
319
|
+
ok: false,
|
|
320
|
+
error: String(err && err.message || err)
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (msg && msg.type === "done") {
|
|
327
|
+
if (finished) return;
|
|
328
|
+
finished = true;
|
|
329
|
+
|
|
330
|
+
const execution = {
|
|
331
|
+
code,
|
|
332
|
+
context: safeCtx,
|
|
333
|
+
seed: (opts.deterministic && typeof opts.deterministic === "object")
|
|
334
|
+
? opts.deterministic.seed
|
|
335
|
+
: (opts.deterministic ? 1 : undefined),
|
|
336
|
+
rpc: rpcLog,
|
|
337
|
+
ok: !!msg.ok,
|
|
338
|
+
result: msg.ok ? msg.result : undefined,
|
|
339
|
+
error: msg.ok ? undefined : msg.error
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
Q.Data.digest("SHA-256", JSON.stringify(execution))
|
|
343
|
+
.then(function (bytes) {
|
|
344
|
+
var hash = Q.Data.toHex(bytes);
|
|
345
|
+
cleanup();
|
|
346
|
+
if (msg.ok) {
|
|
347
|
+
resolve({
|
|
348
|
+
result: msg.result,
|
|
349
|
+
hash
|
|
350
|
+
});
|
|
351
|
+
} else {
|
|
352
|
+
const err = new Error(msg.error || "Sandbox error");
|
|
353
|
+
err.hash = hash;
|
|
354
|
+
reject(err);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
worker.onerror = function (err) {
|
|
361
|
+
cleanup();
|
|
362
|
+
reject(err.message || String(err));
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
timer = setTimeout(function () {
|
|
366
|
+
cleanup();
|
|
367
|
+
reject(new Error("Worker timeout / infinite loop"));
|
|
368
|
+
}, timeoutMs);
|
|
369
|
+
|
|
370
|
+
worker.postMessage({
|
|
371
|
+
code,
|
|
372
|
+
context: safeCtx,
|
|
373
|
+
methodNames,
|
|
374
|
+
deterministic: opts.deterministic || false
|
|
375
|
+
});
|
|
376
|
+
}.bind(this));
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
let runner;
|
|
380
|
+
if (options.name) {
|
|
381
|
+
runner = Q.Sandbox._runners[options.name];
|
|
382
|
+
if (!runner) {
|
|
383
|
+
runner = new SandboxRunner(options);
|
|
384
|
+
Q.Sandbox._runners[options.name] = runner;
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
runner = new SandboxRunner(options);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return runner.run(code, context, methods, options);
|
|
391
|
+
};
|
|
392
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Q.exports(function (Q) {
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Registers a Custom Element (Web Component) for a Q tool.
|
|
5
|
+
* Only active when Q.Tool.define.components === true.
|
|
6
|
+
*
|
|
7
|
+
* Equivalent to writing:
|
|
8
|
+
* <div class="Q_tool Streams_chat_tool" data-streams-chat='{"publisherId":"NYU"}'></div>
|
|
9
|
+
* and having Q.activate() find it. The web component syntax is purely a
|
|
10
|
+
* translation shim — connectedCallback dresses the element and calls Q.activate(this).
|
|
11
|
+
*
|
|
12
|
+
* Attribute → option mapping:
|
|
13
|
+
* - foo-bar="2" → nested: options.foo.bar = 2
|
|
14
|
+
* - fooBar="2" → flat if schema declares it, else options.fooBar = 2
|
|
15
|
+
* - baz → true (bare attribute, no value)
|
|
16
|
+
*
|
|
17
|
+
* Schema is declared as the last element of stateKeys if it's a plain object:
|
|
18
|
+
* stateKeys = ["editable", { count: Q.Types.Integer, visible: Q.Types.Boolean }]
|
|
19
|
+
* Leaf nodes have a .from(string) method. Branch nodes are plain objects without .from().
|
|
20
|
+
* Without a schema, automagic inference handles bool/int/float/JSON/string.
|
|
21
|
+
*
|
|
22
|
+
* @method Q.Tool.define.component
|
|
23
|
+
* @param {String} name Tool name e.g. "Streams/chat"
|
|
24
|
+
* @param {Function} ctor Tool constructor (already registered)
|
|
25
|
+
*/
|
|
26
|
+
return function Q_Tool_define_component(name, ctor) {
|
|
27
|
+
if (typeof customElements === 'undefined') {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// "Streams/chat" -> "streams-chat"
|
|
32
|
+
var tagName = name.toLowerCase().replace(/[/_]/g, '-');
|
|
33
|
+
|
|
34
|
+
if (customElements.get(tagName)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Extract schema from last element of stateKeys if it's a plain object
|
|
39
|
+
var stateKeys = ctor.stateKeys;
|
|
40
|
+
var schema = null;
|
|
41
|
+
if (Array.isArray(stateKeys) && stateKeys.length) {
|
|
42
|
+
var last = stateKeys[stateKeys.length - 1];
|
|
43
|
+
if (Q.isPlainObject(last)) {
|
|
44
|
+
schema = last;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// attrTypeMap: full-hyphenated-path -> Q.Types.X descriptor
|
|
49
|
+
// attrNameMap: full-hyphenated-path -> key path array into options
|
|
50
|
+
var attrTypeMap = {};
|
|
51
|
+
var attrNameMap = {};
|
|
52
|
+
if (schema) {
|
|
53
|
+
_flattenSchema(schema, [], attrTypeMap, attrNameMap);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Recursively walk schema. Leaf nodes have .from(), branch nodes don't.
|
|
58
|
+
* Keys into the maps are the FULL hyphenated path e.g. "foo-bar-baz"
|
|
59
|
+
* to avoid collisions between keys at different nesting levels.
|
|
60
|
+
*/
|
|
61
|
+
function _flattenSchema(node, path, typeMap, nameMap) {
|
|
62
|
+
for (var k in node) {
|
|
63
|
+
if (!node.hasOwnProperty(k)) continue;
|
|
64
|
+
var val = node[k];
|
|
65
|
+
var newPath = path.concat([k]);
|
|
66
|
+
var hyphenPath = newPath.map(_camelToHyphen).join('-');
|
|
67
|
+
if (val && typeof val.from === 'function') {
|
|
68
|
+
typeMap[hyphenPath] = val;
|
|
69
|
+
nameMap[hyphenPath] = newPath;
|
|
70
|
+
} else if (Q.isPlainObject(val)) {
|
|
71
|
+
_flattenSchema(val, newPath, typeMap, nameMap);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _camelToHyphen(str) {
|
|
77
|
+
return str.replace(/([A-Z])/g, function(c) {
|
|
78
|
+
return '-' + c.toLowerCase();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolve one attribute name+value to { path, value }.
|
|
84
|
+
* Resolution order:
|
|
85
|
+
* 1. Full hyphenated path match in schema (e.g. "foo-bar" -> ["foo","bar"])
|
|
86
|
+
* 2. Hyphen-split into nested path
|
|
87
|
+
* 3. Single segment: flat key preserving original casing
|
|
88
|
+
* Bare attribute (no value) -> true, or type.from('') if schema declares it.
|
|
89
|
+
*/
|
|
90
|
+
function _resolveAttr(attrName, attrValue) {
|
|
91
|
+
var lower = attrName.toLowerCase();
|
|
92
|
+
var type = attrTypeMap[lower] || null;
|
|
93
|
+
var path = attrNameMap[lower] || null;
|
|
94
|
+
|
|
95
|
+
if (!path) {
|
|
96
|
+
var parts = lower.split('-');
|
|
97
|
+
path = parts.length > 1 ? parts : [attrName];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
var converted;
|
|
101
|
+
if (attrValue === null) {
|
|
102
|
+
// bare attribute
|
|
103
|
+
converted = type ? type.from('') : true;
|
|
104
|
+
} else if (type) {
|
|
105
|
+
converted = type.from(attrValue);
|
|
106
|
+
} else {
|
|
107
|
+
converted = _infer(attrValue);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { path: path, value: converted };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Automagic inference when no schema type is declared.
|
|
115
|
+
* Order: "true"/"false" -> Boolean, integer, float, JSON, String.
|
|
116
|
+
*/
|
|
117
|
+
function _infer(str) {
|
|
118
|
+
if (str === 'true') return true;
|
|
119
|
+
if (str === 'false') return false;
|
|
120
|
+
if (str === 'null') return null;
|
|
121
|
+
if (str === '') return true;
|
|
122
|
+
if (/^-?\d+$/.test(str)) return parseInt(str, 10);
|
|
123
|
+
if (/^-?\d*\.\d+$/.test(str)) return parseFloat(str);
|
|
124
|
+
if (str[0] === '{' || str[0] === '[') {
|
|
125
|
+
try { return JSON.parse(str); } catch(e) {}
|
|
126
|
+
}
|
|
127
|
+
return str;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build options object from all non-standard attributes on the element.
|
|
132
|
+
* Skips: id, class, style, slot, and all data-* except the tool's own.
|
|
133
|
+
* The tool's own data-* attribute (e.g. data-streams-chat) is parsed as
|
|
134
|
+
* a raw JSON blob and merged as the base, with individual attrs on top.
|
|
135
|
+
*/
|
|
136
|
+
function _attrsToOptions(element) {
|
|
137
|
+
var options = {};
|
|
138
|
+
var skip = { id: 1, 'class': 1, style: 1, slot: 1 };
|
|
139
|
+
var ownDataAttr = 'data-' + tagName;
|
|
140
|
+
var attrs = element.attributes;
|
|
141
|
+
|
|
142
|
+
for (var i = 0; i < attrs.length; i++) {
|
|
143
|
+
var attr = attrs[i];
|
|
144
|
+
var aName = attr.name;
|
|
145
|
+
|
|
146
|
+
if (skip[aName]) continue;
|
|
147
|
+
|
|
148
|
+
// Legacy JSON blob on the tool's own data- attr: merge as base
|
|
149
|
+
if (aName === ownDataAttr) {
|
|
150
|
+
try {
|
|
151
|
+
var blob = JSON.parse(attr.value);
|
|
152
|
+
if (Q.isPlainObject(blob)) {
|
|
153
|
+
Q.extend(options, blob);
|
|
154
|
+
}
|
|
155
|
+
} catch(e) {}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Skip other data-* passthrough attributes
|
|
160
|
+
if (aName.slice(0, 5) === 'data-') continue;
|
|
161
|
+
|
|
162
|
+
var resolved = _resolveAttr(aName, attr.value === '' ? null : attr.value);
|
|
163
|
+
Q.setObject(resolved.path, resolved.value, options);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return options;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// observedAttributes: only schema-declared attrs trigger attributeChangedCallback.
|
|
170
|
+
// Undeclared attrs are still read at connectedCallback time via _attrsToOptions.
|
|
171
|
+
var observedAttrNames = Object.keys(attrTypeMap);
|
|
172
|
+
|
|
173
|
+
var ntt = name.split('/').join('_');
|
|
174
|
+
|
|
175
|
+
class ToolElement extends HTMLElement {
|
|
176
|
+
|
|
177
|
+
connectedCallback() {
|
|
178
|
+
this.classList.add('Q_tool', ntt + '_tool');
|
|
179
|
+
var options = _attrsToOptions(this);
|
|
180
|
+
if (!Q.isEmpty(options)) {
|
|
181
|
+
this.setAttribute(
|
|
182
|
+
'data-' + tagName,
|
|
183
|
+
JSON.stringify(options)
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
Q.activate(this);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
disconnectedCallback() {
|
|
190
|
+
if (this.getAttribute('data-Q-retain') !== null) return;
|
|
191
|
+
Q.Tool.remove(this);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
attributeChangedCallback(attrName, oldVal, newVal) {
|
|
195
|
+
if (oldVal === newVal) return;
|
|
196
|
+
// Fires only for schema-declared attributes after initial connection.
|
|
197
|
+
// Before connection, the change will be picked up by connectedCallback.
|
|
198
|
+
var tool = Q.Tool.from(this, name);
|
|
199
|
+
if (!tool) return;
|
|
200
|
+
var resolved = _resolveAttr(attrName, newVal === '' ? null : newVal);
|
|
201
|
+
var update = {};
|
|
202
|
+
Q.setObject(resolved.path, resolved.value, update);
|
|
203
|
+
tool.setState(update);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
static get observedAttributes() {
|
|
207
|
+
return observedAttrNames;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
customElements.define(tagName, ToolElement);
|
|
213
|
+
} catch(e) {
|
|
214
|
+
console.warn('Q.Tool: could not register <' + tagName + '>:', e);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Q.globalMemoryWalk = function (filterFn, options) {
|
|
2
|
+
options = options || {};
|
|
3
|
+
var seen = new WeakSet();
|
|
4
|
+
var found = new Set();
|
|
5
|
+
var pathMap = new WeakMap();
|
|
6
|
+
|
|
7
|
+
var maxDepth = options.maxDepth || 5;
|
|
8
|
+
var includeStack = options.includeStack || false;
|
|
9
|
+
var logEvery = options.logEvery || 100;
|
|
10
|
+
var startingKeys = Q.globalNamesAdded
|
|
11
|
+
? Q.globalNamesAdded()
|
|
12
|
+
: Object.keys(window);
|
|
13
|
+
|
|
14
|
+
let totalChecked = 0;
|
|
15
|
+
let matchesFound = 0;
|
|
16
|
+
|
|
17
|
+
function walk(obj, path = 'window', depth = 0) {
|
|
18
|
+
if (!obj || typeof obj !== 'object') return;
|
|
19
|
+
if (seen.has(obj)) return;
|
|
20
|
+
seen.add(obj);
|
|
21
|
+
|
|
22
|
+
totalChecked++;
|
|
23
|
+
if (totalChecked % logEvery === 0) {
|
|
24
|
+
console.log(`Checked ${totalChecked} objects, found ${matchesFound}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (filterFn(obj)) {
|
|
28
|
+
found.add(obj);
|
|
29
|
+
matchesFound++;
|
|
30
|
+
if (includeStack) {
|
|
31
|
+
pathMap.set(obj, path);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (depth >= maxDepth) return;
|
|
36
|
+
|
|
37
|
+
var skipKeys = obj instanceof HTMLElement
|
|
38
|
+
? new Set([
|
|
39
|
+
'parentNode', 'parentElement', 'nextSibling', 'previousSibling',
|
|
40
|
+
'firstChild', 'lastChild', 'children', 'childNodes',
|
|
41
|
+
'ownerDocument', 'style', 'classList', 'dataset',
|
|
42
|
+
'attributes', 'innerHTML', 'outerHTML',
|
|
43
|
+
'nextElementSibling', 'previousElementSibling'
|
|
44
|
+
])
|
|
45
|
+
: null;
|
|
46
|
+
|
|
47
|
+
for (var key in obj) {
|
|
48
|
+
if (skipKeys && skipKeys.has(key)) continue;
|
|
49
|
+
try {
|
|
50
|
+
walk(obj[key], path + '.' + key, depth + 1);
|
|
51
|
+
} catch (e) {}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let i = 0;
|
|
56
|
+
function nextBatch() {
|
|
57
|
+
var batchSize = 10;
|
|
58
|
+
var end = Math.min(i + batchSize, startingKeys.length);
|
|
59
|
+
|
|
60
|
+
for (; i < end; i++) {
|
|
61
|
+
try {
|
|
62
|
+
walk(window[startingKeys[i]], 'window.' + startingKeys[i], 0);
|
|
63
|
+
} catch (e) {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (i < startingKeys.length) {
|
|
67
|
+
setTimeout(nextBatch, 0); // Schedule next batch
|
|
68
|
+
} else {
|
|
69
|
+
console.log(`Done. Found ${matchesFound} retained objects.`);
|
|
70
|
+
if (includeStack) {
|
|
71
|
+
console.log([...found].map(obj => ({
|
|
72
|
+
object: obj,
|
|
73
|
+
path: pathMap.get(obj)
|
|
74
|
+
})));
|
|
75
|
+
} else {
|
|
76
|
+
console.log([...found]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
nextBatch();
|
|
82
|
+
};
|