ai-evaluate 0.1.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/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-test.log +51 -0
- package/README.md +420 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-final.json +4 -0
- package/coverage/evaluate.ts.html +574 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +146 -0
- package/coverage/index.ts.html +145 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/worker-template.ts.html +1948 -0
- package/dist/evaluate.d.ts +62 -0
- package/dist/evaluate.d.ts.map +1 -0
- package/dist/evaluate.js +188 -0
- package/dist/evaluate.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +165 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/worker-template.d.ts +40 -0
- package/dist/worker-template.d.ts.map +1 -0
- package/dist/worker-template.js +3628 -0
- package/dist/worker-template.js.map +1 -0
- package/package.json +46 -0
- package/src/evaluate.ts +217 -0
- package/src/index.ts +21 -0
- package/src/types.ts +174 -0
- package/src/worker-template.ts +3677 -0
- package/test/evaluate-extended.test.ts +469 -0
- package/test/evaluate.test.ts +253 -0
- package/test/index.test.ts +95 -0
- package/test/worker-template.test.ts +430 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,3628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker template for sandbox execution
|
|
3
|
+
*
|
|
4
|
+
* This code is stringified and sent to the worker loader.
|
|
5
|
+
* It uses the TEST service binding (ai-tests) for assertions and test running.
|
|
6
|
+
*
|
|
7
|
+
* The user's code (module, tests, script) is embedded directly into
|
|
8
|
+
* the worker source - no eval() or new Function() needed. The security
|
|
9
|
+
* comes from running in an isolated V8 context via worker_loaders.
|
|
10
|
+
*
|
|
11
|
+
* Routes:
|
|
12
|
+
* - POST /execute - Run tests and scripts, return results
|
|
13
|
+
* - POST /rpc or WebSocket upgrade - capnweb RPC to module exports
|
|
14
|
+
* - GET / - Return info about available exports
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Generate SDK code for injection into sandbox
|
|
18
|
+
*
|
|
19
|
+
* Supports two modes:
|
|
20
|
+
* - local: In-memory implementations (for testing without network)
|
|
21
|
+
* - remote: RPC-based implementations (for production/integration tests)
|
|
22
|
+
*/
|
|
23
|
+
function generateSDKCode(config = {}) {
|
|
24
|
+
// Use local mode by default for sandboxed execution
|
|
25
|
+
if (config.context === 'remote') {
|
|
26
|
+
return generateRemoteSDKCode(config);
|
|
27
|
+
}
|
|
28
|
+
return generateLocalSDKCode(config);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generate local SDK code with in-memory implementations
|
|
32
|
+
*
|
|
33
|
+
* Implements APIs that align with ai-database (MemoryDB) and ai-workflows:
|
|
34
|
+
* - MongoDB-style query operators ($gt, $gte, $lt, $in, $regex, etc.)
|
|
35
|
+
* - URL resolution and identifier parsing
|
|
36
|
+
* - upsert, generate, forEach methods
|
|
37
|
+
* - Typed collection accessors (db.Users.find(), etc.)
|
|
38
|
+
* - Workflow event/schedule patterns
|
|
39
|
+
*/
|
|
40
|
+
function generateLocalSDKCode(config = {}) {
|
|
41
|
+
const ns = config.ns || 'default';
|
|
42
|
+
const aiGatewayUrl = config.aiGatewayUrl || '';
|
|
43
|
+
const aiGatewayToken = config.aiGatewayToken || '';
|
|
44
|
+
return `
|
|
45
|
+
// ============================================================
|
|
46
|
+
// Local SDK - In-memory implementation (aligned with ai-database/ai-workflows)
|
|
47
|
+
// ============================================================
|
|
48
|
+
|
|
49
|
+
const __SDK_CONFIG__ = {
|
|
50
|
+
ns: '${ns}',
|
|
51
|
+
aiGatewayUrl: '${aiGatewayUrl}',
|
|
52
|
+
aiGatewayToken: '${aiGatewayToken}'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// In-memory database storage (mirrors MemoryDB structure)
|
|
56
|
+
const __db_things__ = new Map();
|
|
57
|
+
const __db_relationships__ = new Map();
|
|
58
|
+
// Indexes for efficient lookups
|
|
59
|
+
const __db_byUrl__ = new Map();
|
|
60
|
+
const __db_byNsType__ = new Map();
|
|
61
|
+
const __db_relFrom__ = new Map();
|
|
62
|
+
const __db_relTo__ = new Map();
|
|
63
|
+
|
|
64
|
+
// ID generator (crypto.randomUUID not available in all environments)
|
|
65
|
+
const __generateId__ = () => {
|
|
66
|
+
const bytes = new Uint8Array(16);
|
|
67
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
68
|
+
crypto.getRandomValues(bytes);
|
|
69
|
+
} else {
|
|
70
|
+
for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
71
|
+
}
|
|
72
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
|
|
73
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant
|
|
74
|
+
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
75
|
+
return hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' + hex.slice(16, 20) + '-' + hex.slice(20);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// URL resolution (mirrors ai-database resolveUrl)
|
|
79
|
+
const __resolveUrl__ = (entity) => {
|
|
80
|
+
if (entity.url) return entity.url;
|
|
81
|
+
return 'https://' + entity.ns + '/' + entity.type + '/' + entity.id;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Parse identifier (mirrors ai-database parseIdentifier)
|
|
85
|
+
const __parseIdentifier__ = (identifier, defaults = {}) => {
|
|
86
|
+
if (identifier.includes('://')) {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = new URL(identifier);
|
|
89
|
+
const parts = parsed.pathname.split('/').filter(Boolean);
|
|
90
|
+
return {
|
|
91
|
+
ns: parsed.host,
|
|
92
|
+
type: parts[0] || '',
|
|
93
|
+
id: parts.slice(1).join('/') || '',
|
|
94
|
+
url: identifier
|
|
95
|
+
};
|
|
96
|
+
} catch { return { ns: defaults.ns, id: identifier }; }
|
|
97
|
+
}
|
|
98
|
+
if (identifier.includes('/')) {
|
|
99
|
+
const parts = identifier.split('/');
|
|
100
|
+
return { ns: defaults.ns, type: parts[0], id: parts.slice(1).join('/') };
|
|
101
|
+
}
|
|
102
|
+
return { ns: defaults.ns, type: defaults.type, id: identifier };
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Extract mdxld metadata from data object
|
|
106
|
+
const __extractType__ = (data) => data && (data.$type || data['@type']);
|
|
107
|
+
const __extractId__ = (data) => data && (data.$id || data['@id']);
|
|
108
|
+
const __extractContext__ = (data) => data && (data.$context || data['@context']);
|
|
109
|
+
|
|
110
|
+
// Index management
|
|
111
|
+
const __indexThing__ = (thing) => {
|
|
112
|
+
const url = __resolveUrl__(thing);
|
|
113
|
+
__db_byUrl__.set(url, thing);
|
|
114
|
+
const nsTypeKey = thing.ns + '/' + thing.type;
|
|
115
|
+
if (!__db_byNsType__.has(nsTypeKey)) __db_byNsType__.set(nsTypeKey, new Set());
|
|
116
|
+
__db_byNsType__.get(nsTypeKey).add(url);
|
|
117
|
+
};
|
|
118
|
+
const __unindexThing__ = (thing) => {
|
|
119
|
+
const url = __resolveUrl__(thing);
|
|
120
|
+
__db_byUrl__.delete(url);
|
|
121
|
+
const nsTypeKey = thing.ns + '/' + thing.type;
|
|
122
|
+
const set = __db_byNsType__.get(nsTypeKey);
|
|
123
|
+
if (set) set.delete(url);
|
|
124
|
+
};
|
|
125
|
+
const __indexRelationship__ = (rel) => {
|
|
126
|
+
if (!__db_relFrom__.has(rel.from)) __db_relFrom__.set(rel.from, new Set());
|
|
127
|
+
__db_relFrom__.get(rel.from).add(rel.id);
|
|
128
|
+
if (!__db_relTo__.has(rel.to)) __db_relTo__.set(rel.to, new Set());
|
|
129
|
+
__db_relTo__.get(rel.to).add(rel.id);
|
|
130
|
+
};
|
|
131
|
+
const __unindexRelationship__ = (rel) => {
|
|
132
|
+
const fromSet = __db_relFrom__.get(rel.from);
|
|
133
|
+
if (fromSet) fromSet.delete(rel.id);
|
|
134
|
+
const toSet = __db_relTo__.get(rel.to);
|
|
135
|
+
if (toSet) toSet.delete(rel.id);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// MongoDB-style query matching (mirrors MemoryDB.matchesQuery)
|
|
139
|
+
const __matchesQuery__ = (thing, options) => {
|
|
140
|
+
if (options.ns && thing.ns !== options.ns) return false;
|
|
141
|
+
if (options.type && thing.type !== options.type) return false;
|
|
142
|
+
if (options.where) {
|
|
143
|
+
for (const [key, value] of Object.entries(options.where)) {
|
|
144
|
+
const thingValue = thing.data[key];
|
|
145
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
146
|
+
for (const [op, opVal] of Object.entries(value)) {
|
|
147
|
+
switch (op) {
|
|
148
|
+
case '$gt': if (!(thingValue > opVal)) return false; break;
|
|
149
|
+
case '$gte': if (!(thingValue >= opVal)) return false; break;
|
|
150
|
+
case '$lt': if (!(thingValue < opVal)) return false; break;
|
|
151
|
+
case '$lte': if (!(thingValue <= opVal)) return false; break;
|
|
152
|
+
case '$ne': if (thingValue === opVal) return false; break;
|
|
153
|
+
case '$in': if (!Array.isArray(opVal) || !opVal.includes(thingValue)) return false; break;
|
|
154
|
+
case '$nin': if (Array.isArray(opVal) && opVal.includes(thingValue)) return false; break;
|
|
155
|
+
case '$exists': if (opVal && thingValue === undefined) return false; if (!opVal && thingValue !== undefined) return false; break;
|
|
156
|
+
case '$regex': if (typeof thingValue !== 'string') return false; const regex = typeof opVal === 'string' ? new RegExp(opVal) : opVal; if (!regex.test(thingValue)) return false; break;
|
|
157
|
+
default: if (thingValue !== value) return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} else if (thingValue !== value) return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Apply query options (sort, limit, offset)
|
|
167
|
+
const __applyQueryOptions__ = (items, options) => {
|
|
168
|
+
let result = [...items];
|
|
169
|
+
if (options.orderBy) {
|
|
170
|
+
const field = options.orderBy;
|
|
171
|
+
const dir = options.order === 'desc' ? -1 : 1;
|
|
172
|
+
result.sort((a, b) => {
|
|
173
|
+
const aVal = a[field] ?? a.data?.[field];
|
|
174
|
+
const bVal = b[field] ?? b.data?.[field];
|
|
175
|
+
if (aVal === undefined && bVal === undefined) return 0;
|
|
176
|
+
if (aVal === undefined) return dir;
|
|
177
|
+
if (bVal === undefined) return -dir;
|
|
178
|
+
if (aVal < bVal) return -dir;
|
|
179
|
+
if (aVal > bVal) return dir;
|
|
180
|
+
return 0;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (options.offset) result = result.slice(options.offset);
|
|
184
|
+
if (options.limit) result = result.slice(0, options.limit);
|
|
185
|
+
return result;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Cosine similarity helper for semantic search
|
|
189
|
+
const __cosineSimilarity__ = (a, b) => {
|
|
190
|
+
if (!a || !b || a.length !== b.length || a.length === 0) return 0;
|
|
191
|
+
let dotProduct = 0, normA = 0, normB = 0;
|
|
192
|
+
for (let i = 0; i < a.length; i++) {
|
|
193
|
+
dotProduct += a[i] * b[i];
|
|
194
|
+
normA += a[i] * a[i];
|
|
195
|
+
normB += b[i] * b[i];
|
|
196
|
+
}
|
|
197
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
198
|
+
return magnitude === 0 ? 0 : dotProduct / magnitude;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Embedding cache for semantic search (stores URL -> embedding)
|
|
202
|
+
const __embeddings__ = new Map();
|
|
203
|
+
|
|
204
|
+
// AI embed helper for semantic search - defined early so __db_core__.search can use it
|
|
205
|
+
// Uses Gemini embedding model (768 dimensions) through AI Gateway
|
|
206
|
+
const __aiEmbed__ = async (text) => {
|
|
207
|
+
if (!__SDK_CONFIG__.aiGatewayUrl) return [];
|
|
208
|
+
try {
|
|
209
|
+
const url = __SDK_CONFIG__.aiGatewayUrl + '/google-ai-studio/v1beta/models/gemini-embedding-001:embedContent';
|
|
210
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
211
|
+
if (__SDK_CONFIG__.aiGatewayToken) {
|
|
212
|
+
headers['cf-aig-authorization'] = 'Bearer ' + __SDK_CONFIG__.aiGatewayToken;
|
|
213
|
+
}
|
|
214
|
+
const response = await fetch(url, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers,
|
|
217
|
+
body: JSON.stringify({
|
|
218
|
+
content: { parts: [{ text }] },
|
|
219
|
+
outputDimensionality: 768
|
|
220
|
+
})
|
|
221
|
+
});
|
|
222
|
+
if (!response.ok) return [];
|
|
223
|
+
const result = await response.json();
|
|
224
|
+
return result.embedding?.values || [];
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.warn('Embedding failed:', err.message);
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Local DB implementation (aligned with ai-database DBClient interface)
|
|
232
|
+
const __db_core__ = {
|
|
233
|
+
ns: __SDK_CONFIG__.ns,
|
|
234
|
+
|
|
235
|
+
async list(options = {}) {
|
|
236
|
+
const results = [];
|
|
237
|
+
for (const thing of __db_things__.values()) {
|
|
238
|
+
if (__matchesQuery__(thing, options)) results.push(thing);
|
|
239
|
+
}
|
|
240
|
+
return __applyQueryOptions__(results, options);
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
async find(options = {}) {
|
|
244
|
+
return this.list(options);
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
async search(options = {}) {
|
|
248
|
+
const query = (options.query || '').toLowerCase();
|
|
249
|
+
const fields = options.fields || ['data'];
|
|
250
|
+
const minScore = options.minScore || 0;
|
|
251
|
+
const results = [];
|
|
252
|
+
|
|
253
|
+
// Semantic search using embeddings
|
|
254
|
+
if (options.semantic && typeof __aiEmbed__ === 'function') {
|
|
255
|
+
const queryEmbedding = await __aiEmbed__(options.query || '');
|
|
256
|
+
if (!queryEmbedding || queryEmbedding.length === 0) {
|
|
257
|
+
// Embedding failed - return text-based results sorted by relevance
|
|
258
|
+
// This handles cases where AI Gateway auth isn't configured
|
|
259
|
+
console.warn('Semantic search: embeddings unavailable, using fuzzy text matching');
|
|
260
|
+
const queryTerms = (options.query || '').toLowerCase().split(/\\s+/);
|
|
261
|
+
const textResults = [];
|
|
262
|
+
for (const thing of __db_things__.values()) {
|
|
263
|
+
if (!__matchesQuery__(thing, options)) continue;
|
|
264
|
+
const content = JSON.stringify(thing.data).toLowerCase();
|
|
265
|
+
// Score based on how many query terms appear in the content
|
|
266
|
+
let score = 0;
|
|
267
|
+
for (const term of queryTerms) {
|
|
268
|
+
if (content.includes(term)) score += 1;
|
|
269
|
+
// Bonus for partial matches
|
|
270
|
+
for (const word of content.split(/[\\s\\W]+/)) {
|
|
271
|
+
if (word.includes(term) || term.includes(word)) score += 0.1;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (score > 0) textResults.push({ thing, score });
|
|
275
|
+
}
|
|
276
|
+
textResults.sort((a, b) => b.score - a.score);
|
|
277
|
+
return __applyQueryOptions__(textResults.map(r => r.thing), options);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const thing of __db_things__.values()) {
|
|
281
|
+
if (!__matchesQuery__(thing, options)) continue;
|
|
282
|
+
|
|
283
|
+
// Get or compute embedding for this thing
|
|
284
|
+
const thingUrl = thing.url || thing.id;
|
|
285
|
+
let thingEmbedding = __embeddings__.get(thingUrl);
|
|
286
|
+
|
|
287
|
+
if (!thingEmbedding) {
|
|
288
|
+
const textContent = JSON.stringify(thing.data);
|
|
289
|
+
thingEmbedding = await __aiEmbed__(textContent);
|
|
290
|
+
if (thingEmbedding && thingEmbedding.length > 0) {
|
|
291
|
+
__embeddings__.set(thingUrl, thingEmbedding);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (thingEmbedding && thingEmbedding.length > 0) {
|
|
296
|
+
const score = __cosineSimilarity__(queryEmbedding, thingEmbedding);
|
|
297
|
+
if (score >= minScore) results.push({ thing, score });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
results.sort((a, b) => b.score - a.score);
|
|
302
|
+
return __applyQueryOptions__(results.map(r => r.thing), options);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Text-based search
|
|
306
|
+
for (const thing of __db_things__.values()) {
|
|
307
|
+
if (!__matchesQuery__(thing, options)) continue;
|
|
308
|
+
const searchIn = fields.includes('data')
|
|
309
|
+
? JSON.stringify(thing.data).toLowerCase()
|
|
310
|
+
: fields.map(f => String(thing[f] || '')).join(' ').toLowerCase();
|
|
311
|
+
if (searchIn.includes(query)) {
|
|
312
|
+
const index = searchIn.indexOf(query);
|
|
313
|
+
const score = 1 - (index / searchIn.length);
|
|
314
|
+
if (score >= minScore) results.push({ thing, score });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
results.sort((a, b) => b.score - a.score);
|
|
318
|
+
return __applyQueryOptions__(results.map(r => r.thing), options);
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
async get(identifier, options = {}) {
|
|
322
|
+
let url;
|
|
323
|
+
try {
|
|
324
|
+
const parsed = __parseIdentifier__(identifier, { ns: __SDK_CONFIG__.ns });
|
|
325
|
+
if (parsed.url) url = parsed.url;
|
|
326
|
+
else if (parsed.ns && parsed.type && parsed.id) url = 'https://' + parsed.ns + '/' + parsed.type + '/' + parsed.id;
|
|
327
|
+
else if (parsed.ns && parsed.id) {
|
|
328
|
+
for (const [thingUrl, thing] of __db_byUrl__) {
|
|
329
|
+
if (thing.ns === parsed.ns && thing.id === parsed.id) return thing;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} catch { }
|
|
333
|
+
if (url) {
|
|
334
|
+
const thing = __db_byUrl__.get(url);
|
|
335
|
+
if (thing) return thing;
|
|
336
|
+
}
|
|
337
|
+
// Try by ID across all things
|
|
338
|
+
for (const thing of __db_things__.values()) {
|
|
339
|
+
if (thing.id === identifier || thing.url === identifier) return thing;
|
|
340
|
+
}
|
|
341
|
+
// Handle create/generate options
|
|
342
|
+
if (options.create || options.generate) {
|
|
343
|
+
const parsed = __parseIdentifier__(identifier, { ns: __SDK_CONFIG__.ns });
|
|
344
|
+
if (options.generate) return this.generate(identifier, typeof options.generate === 'object' ? options.generate : {});
|
|
345
|
+
const data = typeof options.create === 'object' ? options.create : {};
|
|
346
|
+
// Prioritize $type from data over URL-derived type
|
|
347
|
+
const type = __extractType__(data) || parsed.type || 'Thing';
|
|
348
|
+
const id = parsed.id || __extractId__(data) || __generateId__();
|
|
349
|
+
return this.create({ ns: parsed.ns || __SDK_CONFIG__.ns, type, id, data });
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
async set(url, data) {
|
|
355
|
+
const existing = __db_byUrl__.get(url);
|
|
356
|
+
if (existing) {
|
|
357
|
+
existing.data = data;
|
|
358
|
+
existing.updatedAt = new Date();
|
|
359
|
+
return existing;
|
|
360
|
+
}
|
|
361
|
+
const parsed = __parseIdentifier__(url, { ns: __SDK_CONFIG__.ns });
|
|
362
|
+
const thing = {
|
|
363
|
+
ns: parsed.ns || __SDK_CONFIG__.ns,
|
|
364
|
+
type: parsed.type || '',
|
|
365
|
+
id: parsed.id || __generateId__(),
|
|
366
|
+
url,
|
|
367
|
+
createdAt: new Date(),
|
|
368
|
+
updatedAt: new Date(),
|
|
369
|
+
data
|
|
370
|
+
};
|
|
371
|
+
__db_things__.set(url, thing);
|
|
372
|
+
__indexThing__(thing);
|
|
373
|
+
return thing;
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
async create(urlOrOptions, dataArg) {
|
|
377
|
+
// URL-first syntax: create('https://...', { $type: 'Post', ... })
|
|
378
|
+
if (typeof urlOrOptions === 'string') {
|
|
379
|
+
const url = urlOrOptions;
|
|
380
|
+
const data = dataArg || {};
|
|
381
|
+
const parsed = __parseIdentifier__(url, { ns: __SDK_CONFIG__.ns });
|
|
382
|
+
// Prioritize $type from data over URL-derived type
|
|
383
|
+
const type = __extractType__(data) || parsed.type || 'Thing';
|
|
384
|
+
const id = parsed.id || __extractId__(data) || __generateId__();
|
|
385
|
+
const context = __extractContext__(data);
|
|
386
|
+
const ns = parsed.ns || __SDK_CONFIG__.ns;
|
|
387
|
+
const cleanData = { ...data };
|
|
388
|
+
delete cleanData.$type; delete cleanData.$id; delete cleanData.$context;
|
|
389
|
+
delete cleanData['@type']; delete cleanData['@id']; delete cleanData['@context'];
|
|
390
|
+
const thingUrl = parsed.url || 'https://' + ns + '/' + type + '/' + id;
|
|
391
|
+
if (__db_byUrl__.has(thingUrl)) throw new Error('Thing already exists: ' + thingUrl);
|
|
392
|
+
const thing = { ns, type, id, url: thingUrl, createdAt: new Date(), updatedAt: new Date(), data: cleanData };
|
|
393
|
+
if (context) thing['@context'] = context;
|
|
394
|
+
__db_things__.set(thingUrl, thing);
|
|
395
|
+
__indexThing__(thing);
|
|
396
|
+
return thing;
|
|
397
|
+
}
|
|
398
|
+
// Options syntax: create({ ns, type, data })
|
|
399
|
+
const options = urlOrOptions;
|
|
400
|
+
const id = options.id || __generateId__();
|
|
401
|
+
const thingUrl = options.url || 'https://' + options.ns + '/' + options.type + '/' + id;
|
|
402
|
+
if (__db_byUrl__.has(thingUrl)) throw new Error('Thing already exists: ' + thingUrl);
|
|
403
|
+
const thing = { ns: options.ns, type: options.type, id, url: thingUrl, createdAt: new Date(), updatedAt: new Date(), data: options.data };
|
|
404
|
+
if (options['@context']) thing['@context'] = options['@context'];
|
|
405
|
+
__db_things__.set(thingUrl, thing);
|
|
406
|
+
__indexThing__(thing);
|
|
407
|
+
return thing;
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
async update(url, options) {
|
|
411
|
+
const parsed = __parseIdentifier__(url, { ns: __SDK_CONFIG__.ns });
|
|
412
|
+
const resolvedUrl = parsed.url || 'https://' + parsed.ns + '/' + parsed.type + '/' + parsed.id;
|
|
413
|
+
const existing = __db_byUrl__.get(resolvedUrl);
|
|
414
|
+
if (!existing) throw new Error('Thing not found: ' + resolvedUrl);
|
|
415
|
+
existing.data = { ...existing.data, ...options.data };
|
|
416
|
+
existing.updatedAt = new Date();
|
|
417
|
+
return existing;
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
async upsert(urlOrOptions, dataArg) {
|
|
421
|
+
if (typeof urlOrOptions === 'string') {
|
|
422
|
+
const parsed = __parseIdentifier__(urlOrOptions, { ns: __SDK_CONFIG__.ns });
|
|
423
|
+
const resolvedUrl = parsed.url || (parsed.ns && parsed.type && parsed.id ? 'https://' + parsed.ns + '/' + parsed.type + '/' + parsed.id : null);
|
|
424
|
+
if (resolvedUrl && __db_byUrl__.has(resolvedUrl)) {
|
|
425
|
+
return this.update(resolvedUrl, { data: dataArg || {} });
|
|
426
|
+
}
|
|
427
|
+
return this.create(urlOrOptions, dataArg);
|
|
428
|
+
}
|
|
429
|
+
const options = urlOrOptions;
|
|
430
|
+
const url = options.url || 'https://' + options.ns + '/' + options.type + '/' + (options.id || __generateId__());
|
|
431
|
+
if (__db_byUrl__.has(url)) return this.update(url, { data: options.data });
|
|
432
|
+
return this.create({ ...options, url });
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
async delete(url) {
|
|
436
|
+
const parsed = __parseIdentifier__(url, { ns: __SDK_CONFIG__.ns });
|
|
437
|
+
let resolvedUrl = parsed.url;
|
|
438
|
+
if (!resolvedUrl && parsed.ns && parsed.type && parsed.id) {
|
|
439
|
+
resolvedUrl = 'https://' + parsed.ns + '/' + parsed.type + '/' + parsed.id;
|
|
440
|
+
}
|
|
441
|
+
if (!resolvedUrl) return false;
|
|
442
|
+
const thing = __db_byUrl__.get(resolvedUrl);
|
|
443
|
+
if (!thing) return false;
|
|
444
|
+
__unindexThing__(thing);
|
|
445
|
+
__db_things__.delete(resolvedUrl);
|
|
446
|
+
// Delete related relationships
|
|
447
|
+
const relIds = new Set([...(__db_relFrom__.get(resolvedUrl) || []), ...(__db_relTo__.get(resolvedUrl) || [])]);
|
|
448
|
+
for (const relId of relIds) {
|
|
449
|
+
const rel = __db_relationships__.get(relId);
|
|
450
|
+
if (rel) { __unindexRelationship__(rel); __db_relationships__.delete(relId); }
|
|
451
|
+
}
|
|
452
|
+
return true;
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
async generate(identifier, options = {}) {
|
|
456
|
+
const parsed = __parseIdentifier__(identifier, { ns: __SDK_CONFIG__.ns });
|
|
457
|
+
const type = parsed.type || 'Thing';
|
|
458
|
+
const id = parsed.id || __generateId__();
|
|
459
|
+
const thing = {
|
|
460
|
+
ns: parsed.ns || __SDK_CONFIG__.ns, type, id,
|
|
461
|
+
url: parsed.url || 'https://' + (parsed.ns || __SDK_CONFIG__.ns) + '/' + type + '/' + id,
|
|
462
|
+
createdAt: new Date(), updatedAt: new Date(),
|
|
463
|
+
data: { _generated: true, _prompt: options.prompt, _model: options.model }
|
|
464
|
+
};
|
|
465
|
+
__db_things__.set(thing.url, thing);
|
|
466
|
+
__indexThing__(thing);
|
|
467
|
+
return thing;
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
async forEach(options, callback) {
|
|
471
|
+
const things = await this.list(options);
|
|
472
|
+
for (const thing of things) await callback(thing);
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
async relate(options) {
|
|
476
|
+
const id = __generateId__();
|
|
477
|
+
const rel = { id, type: options.type, from: options.from, to: options.to, createdAt: new Date(), data: options.data };
|
|
478
|
+
__db_relationships__.set(id, rel);
|
|
479
|
+
__indexRelationship__(rel);
|
|
480
|
+
return rel;
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
async unrelate(from, type, to) {
|
|
484
|
+
for (const [id, rel] of __db_relationships__) {
|
|
485
|
+
if (rel.from === from && rel.type === type && rel.to === to) {
|
|
486
|
+
__unindexRelationship__(rel);
|
|
487
|
+
__db_relationships__.delete(id);
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return false;
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
async related(url, relationshipType, direction = 'both') {
|
|
495
|
+
const parsed = __parseIdentifier__(url, { ns: __SDK_CONFIG__.ns });
|
|
496
|
+
const resolvedUrl = parsed.url || 'https://' + parsed.ns + '/' + parsed.type + '/' + parsed.id;
|
|
497
|
+
const relatedUrls = new Set();
|
|
498
|
+
if (direction === 'from' || direction === 'both') {
|
|
499
|
+
const fromRels = __db_relFrom__.get(resolvedUrl);
|
|
500
|
+
if (fromRels) {
|
|
501
|
+
for (const relId of fromRels) {
|
|
502
|
+
const rel = __db_relationships__.get(relId);
|
|
503
|
+
if (rel && (!relationshipType || rel.type === relationshipType)) relatedUrls.add(rel.to);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (direction === 'to' || direction === 'both') {
|
|
508
|
+
const toRels = __db_relTo__.get(resolvedUrl);
|
|
509
|
+
if (toRels) {
|
|
510
|
+
for (const relId of toRels) {
|
|
511
|
+
const rel = __db_relationships__.get(relId);
|
|
512
|
+
if (rel && (!relationshipType || rel.type === relationshipType)) relatedUrls.add(rel.from);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const results = [];
|
|
517
|
+
for (const relatedUrl of relatedUrls) {
|
|
518
|
+
const thing = __db_byUrl__.get(relatedUrl);
|
|
519
|
+
if (thing) results.push(thing);
|
|
520
|
+
}
|
|
521
|
+
return results;
|
|
522
|
+
},
|
|
523
|
+
|
|
524
|
+
async relationships(url, type, direction = 'both') {
|
|
525
|
+
const parsed = __parseIdentifier__(url, { ns: __SDK_CONFIG__.ns });
|
|
526
|
+
const resolvedUrl = parsed.url || 'https://' + parsed.ns + '/' + parsed.type + '/' + parsed.id;
|
|
527
|
+
const results = [];
|
|
528
|
+
if (direction === 'from' || direction === 'both') {
|
|
529
|
+
const fromRels = __db_relFrom__.get(resolvedUrl);
|
|
530
|
+
if (fromRels) {
|
|
531
|
+
for (const relId of fromRels) {
|
|
532
|
+
const rel = __db_relationships__.get(relId);
|
|
533
|
+
if (rel && (!type || rel.type === type)) results.push(rel);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (direction === 'to' || direction === 'both') {
|
|
538
|
+
const toRels = __db_relTo__.get(resolvedUrl);
|
|
539
|
+
if (toRels) {
|
|
540
|
+
for (const relId of toRels) {
|
|
541
|
+
const rel = __db_relationships__.get(relId);
|
|
542
|
+
if (rel && (!type || rel.type === type)) results.push(rel);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return results;
|
|
547
|
+
},
|
|
548
|
+
|
|
549
|
+
clear() {
|
|
550
|
+
__db_things__.clear(); __db_relationships__.clear();
|
|
551
|
+
__db_byUrl__.clear(); __db_byNsType__.clear();
|
|
552
|
+
__db_relFrom__.clear(); __db_relTo__.clear();
|
|
553
|
+
},
|
|
554
|
+
|
|
555
|
+
stats() {
|
|
556
|
+
return { things: __db_things__.size, relationships: __db_relationships__.size };
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// Typed collection accessor (db.Users, db.Posts, etc.) - mirrors ai-database TypedDBOperations
|
|
561
|
+
const db = new Proxy(__db_core__, {
|
|
562
|
+
get: (target, prop) => {
|
|
563
|
+
if (prop in target) return target[prop];
|
|
564
|
+
if (prop === 'then' || prop === 'catch' || prop === 'finally') return undefined;
|
|
565
|
+
// Return a collection accessor for the type
|
|
566
|
+
const type = String(prop);
|
|
567
|
+
const collectionNs = __SDK_CONFIG__.ns;
|
|
568
|
+
const makeUrl = (id) => 'https://' + collectionNs + '/' + type + '/' + id;
|
|
569
|
+
return {
|
|
570
|
+
async list(options = {}) {
|
|
571
|
+
return __db_core__.list({ ...options, type, ns: collectionNs });
|
|
572
|
+
},
|
|
573
|
+
async find(options = {}) {
|
|
574
|
+
return __db_core__.find({ ...options, type, ns: collectionNs });
|
|
575
|
+
},
|
|
576
|
+
async search(options) {
|
|
577
|
+
return __db_core__.search({ ...options, type, ns: collectionNs });
|
|
578
|
+
},
|
|
579
|
+
async get(id, options = {}) {
|
|
580
|
+
return __db_core__.get(makeUrl(id), options);
|
|
581
|
+
},
|
|
582
|
+
async create(idOrData, data) {
|
|
583
|
+
if (typeof idOrData === 'string') {
|
|
584
|
+
return __db_core__.create({ ns: collectionNs, type, id: idOrData, data: data || {} });
|
|
585
|
+
}
|
|
586
|
+
const extractedId = __extractId__(idOrData);
|
|
587
|
+
return __db_core__.create({ ns: collectionNs, type, id: extractedId || __generateId__(), data: idOrData });
|
|
588
|
+
},
|
|
589
|
+
async update(id, data) {
|
|
590
|
+
return __db_core__.update(makeUrl(id), { data });
|
|
591
|
+
},
|
|
592
|
+
async upsert(idOrData, data) {
|
|
593
|
+
if (typeof idOrData === 'string') {
|
|
594
|
+
return __db_core__.upsert({ ns: collectionNs, type, id: idOrData, data: data || {} });
|
|
595
|
+
}
|
|
596
|
+
const extractedId = __extractId__(idOrData);
|
|
597
|
+
return __db_core__.upsert({ ns: collectionNs, type, id: extractedId || __generateId__(), data: idOrData });
|
|
598
|
+
},
|
|
599
|
+
async delete(id) {
|
|
600
|
+
return __db_core__.delete(makeUrl(id));
|
|
601
|
+
},
|
|
602
|
+
async forEach(optionsOrCallback, callback) {
|
|
603
|
+
if (typeof optionsOrCallback === 'function') {
|
|
604
|
+
return __db_core__.forEach({ type, ns: collectionNs }, optionsOrCallback);
|
|
605
|
+
}
|
|
606
|
+
return __db_core__.forEach({ ...optionsOrCallback, type, ns: collectionNs }, callback);
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// AI Gateway client - makes real API calls through Cloudflare AI Gateway
|
|
613
|
+
const __aiGateway__ = {
|
|
614
|
+
async fetch(provider, endpoint, body, extraHeaders = {}) {
|
|
615
|
+
if (!__SDK_CONFIG__.aiGatewayUrl) {
|
|
616
|
+
throw new Error('AI Gateway not configured. Set AI_GATEWAY_URL environment variable.');
|
|
617
|
+
}
|
|
618
|
+
const url = __SDK_CONFIG__.aiGatewayUrl + '/' + provider + endpoint;
|
|
619
|
+
const headers = {
|
|
620
|
+
'Content-Type': 'application/json',
|
|
621
|
+
...extraHeaders
|
|
622
|
+
};
|
|
623
|
+
if (__SDK_CONFIG__.aiGatewayToken) {
|
|
624
|
+
// Use cf-aig-authorization header for AI Gateway with stored credentials
|
|
625
|
+
headers['cf-aig-authorization'] = 'Bearer ' + __SDK_CONFIG__.aiGatewayToken;
|
|
626
|
+
}
|
|
627
|
+
const response = await fetch(url, {
|
|
628
|
+
method: 'POST',
|
|
629
|
+
headers,
|
|
630
|
+
body: JSON.stringify(body)
|
|
631
|
+
});
|
|
632
|
+
if (!response.ok) {
|
|
633
|
+
const error = await response.text();
|
|
634
|
+
throw new Error('AI Gateway error: ' + response.status + ' ' + error);
|
|
635
|
+
}
|
|
636
|
+
return response.json();
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// AI implementation - uses AI Gateway for real API calls (AWS Bedrock for Anthropic models)
|
|
641
|
+
const ai = {
|
|
642
|
+
async generate(prompt, options = {}) {
|
|
643
|
+
// Default to Claude 4.5 Opus via AWS Bedrock
|
|
644
|
+
const model = options.model || 'anthropic.claude-opus-4-5-20251101-v1:0';
|
|
645
|
+
const result = await __aiGateway__.fetch('aws-bedrock', '/model/' + model + '/converse', {
|
|
646
|
+
messages: [{ role: 'user', content: [{ text: prompt }] }],
|
|
647
|
+
inferenceConfig: { maxTokens: options.maxTokens || 1024 }
|
|
648
|
+
});
|
|
649
|
+
const text = result.output?.message?.content?.[0]?.text || '';
|
|
650
|
+
return { text, model, usage: result.usage };
|
|
651
|
+
},
|
|
652
|
+
async embed(text, options = {}) {
|
|
653
|
+
// Use Gemini embedding model (768 dimensions) via AI Gateway
|
|
654
|
+
const dimensions = options.dimensions || 768;
|
|
655
|
+
const result = await __aiGateway__.fetch('google-ai-studio', '/v1beta/models/gemini-embedding-001:embedContent', {
|
|
656
|
+
content: { parts: [{ text }] },
|
|
657
|
+
outputDimensionality: dimensions
|
|
658
|
+
});
|
|
659
|
+
return result.embedding?.values || [];
|
|
660
|
+
},
|
|
661
|
+
async embedMany(texts, options = {}) {
|
|
662
|
+
const dimensions = options.dimensions || 768;
|
|
663
|
+
const embeddings = [];
|
|
664
|
+
for (const text of texts) {
|
|
665
|
+
const result = await __aiGateway__.fetch('google-ai-studio', '/v1beta/models/gemini-embedding-001:embedContent', {
|
|
666
|
+
content: { parts: [{ text }] },
|
|
667
|
+
outputDimensionality: dimensions
|
|
668
|
+
});
|
|
669
|
+
embeddings.push(result.embedding?.values || []);
|
|
670
|
+
}
|
|
671
|
+
return embeddings;
|
|
672
|
+
},
|
|
673
|
+
async chat(messages, options = {}) {
|
|
674
|
+
// Default to Claude 4.5 Opus via AWS Bedrock
|
|
675
|
+
const model = options.model || 'anthropic.claude-opus-4-5-20251101-v1:0';
|
|
676
|
+
const result = await __aiGateway__.fetch('aws-bedrock', '/model/' + model + '/converse', {
|
|
677
|
+
messages: messages.map(m => ({ role: m.role, content: [{ text: m.content }] })),
|
|
678
|
+
inferenceConfig: { maxTokens: options.maxTokens || 1024 }
|
|
679
|
+
});
|
|
680
|
+
const content = result.output?.message?.content?.[0]?.text || '';
|
|
681
|
+
return { role: 'assistant', content, usage: result.usage };
|
|
682
|
+
},
|
|
683
|
+
async complete(prompt, options = {}) {
|
|
684
|
+
const result = await ai.generate(prompt, options);
|
|
685
|
+
return result.text;
|
|
686
|
+
},
|
|
687
|
+
async classify(text, labels, options = {}) {
|
|
688
|
+
const prompt = 'Classify the following text into one of these categories: ' + labels.join(', ') + '\\n\\nText: ' + text + '\\n\\nRespond with just the category name.';
|
|
689
|
+
const result = await ai.generate(prompt, { ...options, maxTokens: 50 });
|
|
690
|
+
const label = labels.find(l => result.text.toLowerCase().includes(l.toLowerCase())) || labels[0];
|
|
691
|
+
return { label, confidence: 0.9 };
|
|
692
|
+
},
|
|
693
|
+
async extract(text, schema, options = {}) {
|
|
694
|
+
const prompt = 'Extract the following information from the text and return as JSON:\\n\\nSchema: ' + JSON.stringify(schema) + '\\n\\nText: ' + text + '\\n\\nRespond with valid JSON only.';
|
|
695
|
+
const result = await ai.generate(prompt, options);
|
|
696
|
+
try {
|
|
697
|
+
return JSON.parse(result.text);
|
|
698
|
+
} catch {
|
|
699
|
+
return { _extracted: true, raw: result.text };
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
async summarize(text, options = {}) {
|
|
703
|
+
const prompt = 'Summarize the following text concisely:\\n\\n' + text;
|
|
704
|
+
const result = await ai.generate(prompt, { ...options, maxTokens: options.maxTokens || 256 });
|
|
705
|
+
return result.text;
|
|
706
|
+
},
|
|
707
|
+
// Create database-aware AI tools (returns array for Claude SDK compatibility)
|
|
708
|
+
createDatabaseTools(database) {
|
|
709
|
+
const dbInstance = database || __db_core__;
|
|
710
|
+
|
|
711
|
+
// Helper for success response in Claude SDK format
|
|
712
|
+
const success = (data) => ({
|
|
713
|
+
content: [{ type: 'text', text: JSON.stringify(data) }]
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
// Helper for error response
|
|
717
|
+
const error = (message) => ({
|
|
718
|
+
content: [{ type: 'text', text: message }],
|
|
719
|
+
isError: true
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
return [
|
|
723
|
+
{
|
|
724
|
+
name: 'mdxdb_list',
|
|
725
|
+
description: 'List documents from the database by type. Returns an array of documents.',
|
|
726
|
+
handler: async (args) => {
|
|
727
|
+
try {
|
|
728
|
+
const { type, prefix, limit = 100 } = args || {};
|
|
729
|
+
const result = await dbInstance.list({ type, prefix, limit });
|
|
730
|
+
return success(result);
|
|
731
|
+
} catch (err) {
|
|
732
|
+
return error('Failed to list documents: ' + (err.message || String(err)));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
name: 'mdxdb_search',
|
|
738
|
+
description: 'Search for documents by query. Supports semantic search when enabled.',
|
|
739
|
+
handler: async (args) => {
|
|
740
|
+
try {
|
|
741
|
+
const { query, type, limit = 10, semantic = false } = args || {};
|
|
742
|
+
const result = await dbInstance.search({ query, type, limit, semantic });
|
|
743
|
+
return success(result);
|
|
744
|
+
} catch (err) {
|
|
745
|
+
return error('Failed to search documents: ' + (err.message || String(err)));
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
name: 'mdxdb_get',
|
|
751
|
+
description: 'Get a specific document by ID. Returns the document or null if not found.',
|
|
752
|
+
handler: async (args) => {
|
|
753
|
+
try {
|
|
754
|
+
const { id, url } = args || {};
|
|
755
|
+
const identifier = url || id;
|
|
756
|
+
if (!identifier) {
|
|
757
|
+
return error('Either id or url is required');
|
|
758
|
+
}
|
|
759
|
+
const doc = await dbInstance.get(identifier);
|
|
760
|
+
if (!doc) {
|
|
761
|
+
return error('Document not found: ' + identifier);
|
|
762
|
+
}
|
|
763
|
+
return success(doc);
|
|
764
|
+
} catch (err) {
|
|
765
|
+
return error('Failed to get document: ' + (err.message || String(err)));
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
name: 'mdxdb_set',
|
|
771
|
+
description: 'Create or update a document. Returns success status.',
|
|
772
|
+
handler: async (args) => {
|
|
773
|
+
try {
|
|
774
|
+
const { id, url, data, content, type } = args || {};
|
|
775
|
+
const identifier = url || id;
|
|
776
|
+
if (!identifier) {
|
|
777
|
+
return error('Either id or url is required');
|
|
778
|
+
}
|
|
779
|
+
// Set data directly - the db.set wraps it in a thing.data property
|
|
780
|
+
// Also include type metadata if provided
|
|
781
|
+
const docData = { ...(data || {}), ...(type ? { $type: type } : {}) };
|
|
782
|
+
await dbInstance.set(identifier, docData);
|
|
783
|
+
return success({ success: true, id: identifier });
|
|
784
|
+
} catch (err) {
|
|
785
|
+
return error('Failed to set document: ' + (err.message || String(err)));
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
name: 'mdxdb_delete',
|
|
791
|
+
description: 'Delete a document by ID. Returns deletion status.',
|
|
792
|
+
handler: async (args) => {
|
|
793
|
+
try {
|
|
794
|
+
const { id, url } = args || {};
|
|
795
|
+
const identifier = url || id;
|
|
796
|
+
if (!identifier) {
|
|
797
|
+
return error('Either id or url is required');
|
|
798
|
+
}
|
|
799
|
+
const result = await dbInstance.delete(identifier);
|
|
800
|
+
return success({ deleted: result.deleted !== false });
|
|
801
|
+
} catch (err) {
|
|
802
|
+
return error('Failed to delete document: ' + (err.message || String(err)));
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
];
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// Add references method to db_core
|
|
811
|
+
__db_core__.references = async function(url, direction = 'both') {
|
|
812
|
+
const rels = await this.relationships(url, undefined, direction);
|
|
813
|
+
const refs = [];
|
|
814
|
+
for (const rel of rels) {
|
|
815
|
+
const targetUrl = direction === 'from' ? rel.to : direction === 'to' ? rel.from : (rel.from === url ? rel.to : rel.from);
|
|
816
|
+
const target = await this.get(targetUrl);
|
|
817
|
+
if (target) refs.push({ ...target, relationship: rel });
|
|
818
|
+
}
|
|
819
|
+
return refs;
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
// ============================================================
|
|
823
|
+
// Hono-compatible HTTP App (for testing)
|
|
824
|
+
// ============================================================
|
|
825
|
+
|
|
826
|
+
// Cache for client component detection to avoid recursion
|
|
827
|
+
const __clientComponentCache__ = new WeakMap();
|
|
828
|
+
|
|
829
|
+
// Check if a function is a client component
|
|
830
|
+
const __isClientComponent__ = (fn) => {
|
|
831
|
+
if (typeof fn !== 'function') return false;
|
|
832
|
+
// Check cache first
|
|
833
|
+
if (__clientComponentCache__.has(fn)) {
|
|
834
|
+
return __clientComponentCache__.get(fn);
|
|
835
|
+
}
|
|
836
|
+
// Mark as processing to prevent recursion
|
|
837
|
+
__clientComponentCache__.set(fn, false);
|
|
838
|
+
|
|
839
|
+
const source = fn.toString();
|
|
840
|
+
let result = false;
|
|
841
|
+
|
|
842
|
+
// Check for 'use client' directive
|
|
843
|
+
if (source.includes("'use client'") || source.includes('"use client"')) {
|
|
844
|
+
result = true;
|
|
845
|
+
}
|
|
846
|
+
// Check for 'use server' directive (explicitly server)
|
|
847
|
+
else if (source.includes("'use server'") || source.includes('"use server"')) {
|
|
848
|
+
result = false;
|
|
849
|
+
}
|
|
850
|
+
// Auto-detect: functions using useState are client components
|
|
851
|
+
else if (source.includes('useState(') || source.includes('useEffect(') || source.includes('useRef(')) {
|
|
852
|
+
result = true;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
__clientComponentCache__.set(fn, result);
|
|
856
|
+
return result;
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
// Simple JSX renderer for Hono JSX components
|
|
860
|
+
const __renderJsx__ = (element) => {
|
|
861
|
+
if (element === null || element === undefined) return '';
|
|
862
|
+
if (typeof element === 'string' || typeof element === 'number') return String(element);
|
|
863
|
+
if (Array.isArray(element)) return element.map(__renderJsx__).join('');
|
|
864
|
+
if (typeof element !== 'object') return String(element);
|
|
865
|
+
|
|
866
|
+
const { type, props } = element;
|
|
867
|
+
if (!type) return '';
|
|
868
|
+
|
|
869
|
+
// Handle function components
|
|
870
|
+
if (typeof type === 'function') {
|
|
871
|
+
const isClient = __isClientComponent__(type);
|
|
872
|
+
try {
|
|
873
|
+
const result = type(props || {});
|
|
874
|
+
const rendered = __renderJsx__(result);
|
|
875
|
+
// Wrap client components with hydration marker
|
|
876
|
+
if (isClient) {
|
|
877
|
+
const componentName = type.name || 'Component';
|
|
878
|
+
return '<div data-hono-hydrate="' + componentName + '">' + rendered + '</div><script>/* hydrate: ' + componentName + ' */</script>';
|
|
879
|
+
}
|
|
880
|
+
return rendered;
|
|
881
|
+
} catch (e) {
|
|
882
|
+
return '<!-- Error: ' + e.message + ' -->';
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Handle HTML elements
|
|
887
|
+
const tag = String(type);
|
|
888
|
+
const attrs = Object.entries(props || {})
|
|
889
|
+
.filter(([k, v]) => k !== 'children' && v !== undefined && v !== null && v !== false)
|
|
890
|
+
.map(([k, v]) => {
|
|
891
|
+
if (v === true) return k;
|
|
892
|
+
const attrName = k === 'className' ? 'class' : k.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
893
|
+
return attrName + '="' + String(v).replace(/"/g, '"') + '"';
|
|
894
|
+
})
|
|
895
|
+
.join(' ');
|
|
896
|
+
|
|
897
|
+
const children = props?.children;
|
|
898
|
+
const childContent = Array.isArray(children)
|
|
899
|
+
? children.map(__renderJsx__).join('')
|
|
900
|
+
: children !== undefined
|
|
901
|
+
? __renderJsx__(children)
|
|
902
|
+
: '';
|
|
903
|
+
|
|
904
|
+
const voidElements = new Set(['br', 'hr', 'img', 'input', 'link', 'meta', 'area', 'base', 'col', 'embed', 'param', 'source', 'track', 'wbr']);
|
|
905
|
+
if (voidElements.has(tag.toLowerCase())) {
|
|
906
|
+
return '<' + tag + (attrs ? ' ' + attrs : '') + ' />';
|
|
907
|
+
}
|
|
908
|
+
return '<' + tag + (attrs ? ' ' + attrs : '') + '>' + childContent + '</' + tag + '>';
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
const __createHonoApp__ = () => {
|
|
912
|
+
const routes = [];
|
|
913
|
+
const middleware = [];
|
|
914
|
+
let notFoundHandler = null;
|
|
915
|
+
let errorHandler = null;
|
|
916
|
+
|
|
917
|
+
// Parse path pattern into regex and param names
|
|
918
|
+
const parsePattern = (pattern) => {
|
|
919
|
+
const params = [];
|
|
920
|
+
// Normalize: remove trailing slash for consistent matching
|
|
921
|
+
let normalizedPattern = pattern.replace(/\\/+$/, '') || '/';
|
|
922
|
+
let regexStr = normalizedPattern
|
|
923
|
+
.replace(/\\*$/, '(?<wildcard>.*)') // Wildcard at end
|
|
924
|
+
.replace(/\\/:([^/]+)\\?/g, (_, name) => { params.push(name); return '(?:/(?<' + name + '>[^/]*))?'; }) // Optional param (makes /segment optional)
|
|
925
|
+
.replace(/:([^/]+)/g, (_, name) => { params.push(name); return '(?<' + name + '>[^/]+)'; }); // Required param
|
|
926
|
+
// Handle trailing slash optionally
|
|
927
|
+
if (!regexStr.endsWith('.*') && !regexStr.endsWith(')?')) {
|
|
928
|
+
regexStr = regexStr + '/?';
|
|
929
|
+
}
|
|
930
|
+
return { regex: new RegExp('^' + regexStr + '(?:\\\\?.*)?$'), params };
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
// Create context object
|
|
934
|
+
const createContext = (req, pathParams, store) => {
|
|
935
|
+
const url = new URL(req.url, 'http://localhost');
|
|
936
|
+
return {
|
|
937
|
+
req: {
|
|
938
|
+
raw: req,
|
|
939
|
+
url: req.url,
|
|
940
|
+
method: req.method,
|
|
941
|
+
path: url.pathname,
|
|
942
|
+
param: (name) => name === '*' ? pathParams.wildcard : pathParams[name],
|
|
943
|
+
query: (name) => url.searchParams.get(name),
|
|
944
|
+
queries: () => {
|
|
945
|
+
const result = {};
|
|
946
|
+
for (const [key, value] of url.searchParams) {
|
|
947
|
+
if (result[key]) {
|
|
948
|
+
if (Array.isArray(result[key])) result[key].push(value);
|
|
949
|
+
else result[key] = [result[key], value];
|
|
950
|
+
} else {
|
|
951
|
+
result[key] = value;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return result;
|
|
955
|
+
},
|
|
956
|
+
header: (name) => req.headers.get(name),
|
|
957
|
+
json: () => req.json(),
|
|
958
|
+
text: () => req.text(),
|
|
959
|
+
arrayBuffer: () => req.arrayBuffer(),
|
|
960
|
+
parseBody: async () => {
|
|
961
|
+
const contentType = req.headers.get('Content-Type') || '';
|
|
962
|
+
if (contentType.includes('application/json')) {
|
|
963
|
+
return req.json();
|
|
964
|
+
}
|
|
965
|
+
if (contentType.includes('multipart/form-data') || contentType.includes('application/x-www-form-urlencoded')) {
|
|
966
|
+
const formData = await req.formData();
|
|
967
|
+
const result = {};
|
|
968
|
+
for (const [key, value] of formData.entries()) {
|
|
969
|
+
result[key] = value;
|
|
970
|
+
}
|
|
971
|
+
return result;
|
|
972
|
+
}
|
|
973
|
+
return req.text();
|
|
974
|
+
},
|
|
975
|
+
},
|
|
976
|
+
// Response methods - text supports optional headers as 3rd param
|
|
977
|
+
text: (body, status = 200, headers = {}) => {
|
|
978
|
+
const h = { 'Content-Type': 'text/plain', ...headers };
|
|
979
|
+
return new Response(body, { status, headers: h });
|
|
980
|
+
},
|
|
981
|
+
json: (data, status = 200) => new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json' } }),
|
|
982
|
+
html: (body, status = 200) => {
|
|
983
|
+
const rendered = typeof body === 'object' ? __renderJsx__(body) : body;
|
|
984
|
+
return new Response(rendered, { status, headers: { 'Content-Type': 'text/html' } });
|
|
985
|
+
},
|
|
986
|
+
body: (data, status = 200) => new Response(data, { status }),
|
|
987
|
+
redirect: (url, status = 302) => new Response(null, { status, headers: { 'Location': url } }),
|
|
988
|
+
notFound: () => new Response('Not Found', { status: 404 }),
|
|
989
|
+
// Streaming helper - returns Response immediately, callback runs async
|
|
990
|
+
stream: (callback) => {
|
|
991
|
+
const { readable, writable } = new TransformStream();
|
|
992
|
+
const writer = writable.getWriter();
|
|
993
|
+
const streamApi = {
|
|
994
|
+
write: async (chunk) => {
|
|
995
|
+
const data = typeof chunk === 'string' ? new TextEncoder().encode(chunk) : chunk;
|
|
996
|
+
await writer.write(data);
|
|
997
|
+
},
|
|
998
|
+
close: async () => await writer.close(),
|
|
999
|
+
pipe: async (rs) => {
|
|
1000
|
+
const reader = rs.getReader();
|
|
1001
|
+
while (true) {
|
|
1002
|
+
const { done, value } = await reader.read();
|
|
1003
|
+
if (done) break;
|
|
1004
|
+
await writer.write(value);
|
|
1005
|
+
}
|
|
1006
|
+
await writer.close();
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
// Run callback async, close stream when done
|
|
1010
|
+
Promise.resolve(callback(streamApi)).then(() => writer.close()).catch(() => writer.close());
|
|
1011
|
+
return new Response(readable, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
|
1012
|
+
},
|
|
1013
|
+
// Status helper
|
|
1014
|
+
status: (code) => {
|
|
1015
|
+
const ctx = createContext(req, pathParams, store);
|
|
1016
|
+
const originalJson = ctx.json;
|
|
1017
|
+
const originalText = ctx.text;
|
|
1018
|
+
const originalHtml = ctx.html;
|
|
1019
|
+
ctx.json = (data) => originalJson(data, code);
|
|
1020
|
+
ctx.text = (body) => originalText(body, code);
|
|
1021
|
+
ctx.html = (body) => originalHtml(body, code);
|
|
1022
|
+
return ctx;
|
|
1023
|
+
},
|
|
1024
|
+
// Header helper
|
|
1025
|
+
header: (name, value) => {
|
|
1026
|
+
store._headers = store._headers || {};
|
|
1027
|
+
store._headers[name] = value;
|
|
1028
|
+
},
|
|
1029
|
+
// Store for middleware
|
|
1030
|
+
set: (key, value) => { store[key] = value; },
|
|
1031
|
+
get: (key) => store[key],
|
|
1032
|
+
};
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
// Register routes
|
|
1036
|
+
const addRoute = (method, path, ...handlers) => {
|
|
1037
|
+
const { regex, params } = parsePattern(path);
|
|
1038
|
+
routes.push({ method: method.toUpperCase(), pattern: path, regex, params, handlers });
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
// Process request through middleware and routes
|
|
1042
|
+
const handleRequest = async (req) => {
|
|
1043
|
+
const url = new URL(req.url, 'http://localhost');
|
|
1044
|
+
const path = url.pathname;
|
|
1045
|
+
const method = req.method;
|
|
1046
|
+
const store = {};
|
|
1047
|
+
|
|
1048
|
+
// Find matching route
|
|
1049
|
+
let matchedRoute = null;
|
|
1050
|
+
let routeParams = {};
|
|
1051
|
+
for (const route of routes) {
|
|
1052
|
+
if (route.method !== method && route.method !== 'ALL') continue;
|
|
1053
|
+
const match = path.match(route.regex);
|
|
1054
|
+
if (match) {
|
|
1055
|
+
matchedRoute = route;
|
|
1056
|
+
routeParams = match.groups || {};
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Collect matching middleware
|
|
1062
|
+
const matchingMiddleware = middleware.filter(mw => {
|
|
1063
|
+
const prefix = mw.prefix.replace('/*', '').replace('*', '');
|
|
1064
|
+
return path.startsWith(prefix) || prefix === '';
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
// Track the response through the chain so middleware can modify headers after next()
|
|
1068
|
+
let chainResponse = null;
|
|
1069
|
+
|
|
1070
|
+
// Create a proper execution chain: middleware -> route handlers
|
|
1071
|
+
const executeChain = async (index) => {
|
|
1072
|
+
// First, run through middleware
|
|
1073
|
+
if (index < matchingMiddleware.length) {
|
|
1074
|
+
const mw = matchingMiddleware[index];
|
|
1075
|
+
const ctx = createContext(req, routeParams, store);
|
|
1076
|
+
const next = async () => {
|
|
1077
|
+
const downstreamResult = await executeChain(index + 1);
|
|
1078
|
+
if (downstreamResult) chainResponse = downstreamResult;
|
|
1079
|
+
return downstreamResult;
|
|
1080
|
+
};
|
|
1081
|
+
const result = await mw.handler(ctx, next);
|
|
1082
|
+
// If middleware returns a response, use it; otherwise use chainResponse
|
|
1083
|
+
if (result) return result;
|
|
1084
|
+
return chainResponse;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Then run the route handler(s)
|
|
1088
|
+
if (!matchedRoute) {
|
|
1089
|
+
// No route matched - use 404 handler
|
|
1090
|
+
if (notFoundHandler) {
|
|
1091
|
+
const ctx = createContext(req, {}, store);
|
|
1092
|
+
return notFoundHandler(ctx);
|
|
1093
|
+
}
|
|
1094
|
+
return new Response('Not Found', { status: 404 });
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const ctx = createContext(req, routeParams, store);
|
|
1098
|
+
for (let i = 0; i < matchedRoute.handlers.length; i++) {
|
|
1099
|
+
const handler = matchedRoute.handlers[i];
|
|
1100
|
+
const isLast = i === matchedRoute.handlers.length - 1;
|
|
1101
|
+
let nextCalled = false;
|
|
1102
|
+
const next = async () => { nextCalled = true; };
|
|
1103
|
+
const result = await handler(ctx, next);
|
|
1104
|
+
if (result) return result;
|
|
1105
|
+
if (!isLast && !nextCalled) break;
|
|
1106
|
+
}
|
|
1107
|
+
return null;
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
// Helper to apply stored headers to response
|
|
1111
|
+
const applyHeaders = (response) => {
|
|
1112
|
+
if (store._headers && response instanceof Response) {
|
|
1113
|
+
for (const [name, value] of Object.entries(store._headers)) {
|
|
1114
|
+
response.headers.set(name, value);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return response;
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
// Execute the chain with error handling
|
|
1121
|
+
try {
|
|
1122
|
+
const result = await executeChain(0);
|
|
1123
|
+
if (result) return applyHeaders(result);
|
|
1124
|
+
// If no result, return 404
|
|
1125
|
+
if (notFoundHandler) {
|
|
1126
|
+
const ctx = createContext(req, {}, store);
|
|
1127
|
+
return applyHeaders(notFoundHandler(ctx));
|
|
1128
|
+
}
|
|
1129
|
+
return new Response('Not Found', { status: 404 });
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
if (errorHandler) {
|
|
1132
|
+
const ctx = createContext(req, {}, store);
|
|
1133
|
+
return applyHeaders(errorHandler(err, ctx));
|
|
1134
|
+
}
|
|
1135
|
+
throw err;
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
const appObj = {
|
|
1140
|
+
get: (path, ...handlers) => addRoute('GET', path, ...handlers),
|
|
1141
|
+
post: (path, ...handlers) => addRoute('POST', path, ...handlers),
|
|
1142
|
+
put: (path, ...handlers) => addRoute('PUT', path, ...handlers),
|
|
1143
|
+
delete: (path, ...handlers) => addRoute('DELETE', path, ...handlers),
|
|
1144
|
+
patch: (path, ...handlers) => addRoute('PATCH', path, ...handlers),
|
|
1145
|
+
all: (path, ...handlers) => addRoute('ALL', path, ...handlers),
|
|
1146
|
+
use: (pathOrHandler, handler) => {
|
|
1147
|
+
const path = typeof pathOrHandler === 'string' ? pathOrHandler : '/*';
|
|
1148
|
+
const h = typeof pathOrHandler === 'function' ? pathOrHandler : handler;
|
|
1149
|
+
const { regex, params } = parsePattern(path);
|
|
1150
|
+
middleware.push({ prefix: path, regex, handler: h });
|
|
1151
|
+
},
|
|
1152
|
+
route: (basePath, subApp) => {
|
|
1153
|
+
// Mount sub-app routes
|
|
1154
|
+
for (const route of subApp._routes || []) {
|
|
1155
|
+
addRoute(route.method, basePath + route.pattern, ...route.handlers);
|
|
1156
|
+
}
|
|
1157
|
+
},
|
|
1158
|
+
// Create a scoped sub-app with a base path
|
|
1159
|
+
basePath: (prefix) => {
|
|
1160
|
+
const subApp = {
|
|
1161
|
+
get: (path, ...handlers) => { addRoute('GET', prefix + path, ...handlers); return subApp; },
|
|
1162
|
+
post: (path, ...handlers) => { addRoute('POST', prefix + path, ...handlers); return subApp; },
|
|
1163
|
+
put: (path, ...handlers) => { addRoute('PUT', prefix + path, ...handlers); return subApp; },
|
|
1164
|
+
delete: (path, ...handlers) => { addRoute('DELETE', prefix + path, ...handlers); return subApp; },
|
|
1165
|
+
patch: (path, ...handlers) => { addRoute('PATCH', prefix + path, ...handlers); return subApp; },
|
|
1166
|
+
all: (path, ...handlers) => { addRoute('ALL', prefix + path, ...handlers); return subApp; },
|
|
1167
|
+
basePath: (subPrefix) => appObj.basePath(prefix + subPrefix),
|
|
1168
|
+
_routes: routes,
|
|
1169
|
+
};
|
|
1170
|
+
return subApp;
|
|
1171
|
+
},
|
|
1172
|
+
// Global 404 handler
|
|
1173
|
+
notFound: (handler) => { notFoundHandler = handler; },
|
|
1174
|
+
// Global error handler
|
|
1175
|
+
onError: (handler) => { errorHandler = handler; },
|
|
1176
|
+
request: async (path, options = {}) => {
|
|
1177
|
+
const url = path.startsWith('http') ? path : 'http://localhost' + path;
|
|
1178
|
+
const req = new Request(url, {
|
|
1179
|
+
method: options.method || 'GET',
|
|
1180
|
+
headers: options.headers || {},
|
|
1181
|
+
body: options.body,
|
|
1182
|
+
});
|
|
1183
|
+
return handleRequest(req);
|
|
1184
|
+
},
|
|
1185
|
+
fetch: handleRequest,
|
|
1186
|
+
_routes: routes,
|
|
1187
|
+
};
|
|
1188
|
+
return appObj;
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
// Create global app instance
|
|
1192
|
+
const app = __createHonoApp__();
|
|
1193
|
+
|
|
1194
|
+
// ============================================================
|
|
1195
|
+
// MDX Rendering Utilities
|
|
1196
|
+
// ============================================================
|
|
1197
|
+
|
|
1198
|
+
// Extract frontmatter from MDX content
|
|
1199
|
+
const __extractFrontmatter__ = (content) => {
|
|
1200
|
+
const match = content.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/);
|
|
1201
|
+
if (!match) return { frontmatter: {}, body: content };
|
|
1202
|
+
try {
|
|
1203
|
+
const fm = {};
|
|
1204
|
+
match[1].split('\\n').forEach(line => {
|
|
1205
|
+
const [key, ...vals] = line.split(':');
|
|
1206
|
+
if (key && vals.length) fm[key.trim()] = vals.join(':').trim();
|
|
1207
|
+
});
|
|
1208
|
+
return { frontmatter: fm, body: match[2] };
|
|
1209
|
+
} catch {
|
|
1210
|
+
return { frontmatter: {}, body: content };
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
|
|
1214
|
+
// render object for MDX
|
|
1215
|
+
const render = {
|
|
1216
|
+
// Render MDX to markdown (strip frontmatter by default)
|
|
1217
|
+
markdown: (content, options = {}) => {
|
|
1218
|
+
const { frontmatter, body } = __extractFrontmatter__(content);
|
|
1219
|
+
if (options.includeFrontmatter) {
|
|
1220
|
+
return content;
|
|
1221
|
+
}
|
|
1222
|
+
return body.trim();
|
|
1223
|
+
},
|
|
1224
|
+
// Extract table of contents from markdown headings
|
|
1225
|
+
toc: (content) => {
|
|
1226
|
+
const { body } = __extractFrontmatter__(content);
|
|
1227
|
+
const headings = [];
|
|
1228
|
+
const regex = /^(#{1,6})\\s+(.+)$/gm;
|
|
1229
|
+
let match;
|
|
1230
|
+
while ((match = regex.exec(body)) !== null) {
|
|
1231
|
+
headings.push({
|
|
1232
|
+
level: match[1].length,
|
|
1233
|
+
text: match[2].trim(),
|
|
1234
|
+
slug: match[2].trim().toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
return headings;
|
|
1238
|
+
},
|
|
1239
|
+
// Render MDX to HTML (simplified)
|
|
1240
|
+
html: (content) => {
|
|
1241
|
+
const { body } = __extractFrontmatter__(content);
|
|
1242
|
+
// Basic markdown to HTML
|
|
1243
|
+
return body
|
|
1244
|
+
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
|
1245
|
+
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
1246
|
+
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
|
1247
|
+
.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')
|
|
1248
|
+
.replace(/\\*(.+?)\\*/g, '<em>$1</em>')
|
|
1249
|
+
.replace(/\\[(.+?)\\]\\((.+?)\\)/g, '<a href="$2">$1</a>')
|
|
1250
|
+
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
|
1251
|
+
.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>')
|
|
1252
|
+
.replace(/^\\n+|\\n+$/g, '');
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
// React-like hooks stubs
|
|
1257
|
+
let __hook_state__ = [];
|
|
1258
|
+
let __hook_index__ = 0;
|
|
1259
|
+
|
|
1260
|
+
const useState = (initial) => {
|
|
1261
|
+
const idx = __hook_index__++;
|
|
1262
|
+
if (__hook_state__[idx] === undefined) {
|
|
1263
|
+
__hook_state__[idx] = initial;
|
|
1264
|
+
}
|
|
1265
|
+
const setState = (newVal) => {
|
|
1266
|
+
__hook_state__[idx] = typeof newVal === 'function' ? newVal(__hook_state__[idx]) : newVal;
|
|
1267
|
+
};
|
|
1268
|
+
return [__hook_state__[idx], setState];
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
const useEffect = (fn, deps) => { /* No-op in server context */ };
|
|
1272
|
+
const useRef = (initial) => ({ current: initial });
|
|
1273
|
+
const useMemo = (fn, deps) => fn();
|
|
1274
|
+
const useCallback = (fn, deps) => fn;
|
|
1275
|
+
|
|
1276
|
+
// Suspense placeholder
|
|
1277
|
+
const Suspense = ({ children, fallback }) => children;
|
|
1278
|
+
|
|
1279
|
+
// Streaming render function
|
|
1280
|
+
const renderToStream = async (element, stream) => {
|
|
1281
|
+
const html = __renderJsx__(element);
|
|
1282
|
+
await stream.write(html);
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
// Serialization utilities for client props
|
|
1286
|
+
const serialize = {
|
|
1287
|
+
clientProps: (props) => {
|
|
1288
|
+
const result = {};
|
|
1289
|
+
for (const [key, value] of Object.entries(props || {})) {
|
|
1290
|
+
if (typeof value === 'function') {
|
|
1291
|
+
result[key] = { __rpc: true, name: value.name || 'anonymous' };
|
|
1292
|
+
} else {
|
|
1293
|
+
result[key] = value;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
return result;
|
|
1297
|
+
},
|
|
1298
|
+
json: JSON.stringify,
|
|
1299
|
+
parse: JSON.parse
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
// Add .isClient getter to Function.prototype for component detection
|
|
1303
|
+
Object.defineProperty(Function.prototype, 'isClient', {
|
|
1304
|
+
get: function() {
|
|
1305
|
+
return __isClientComponent__(this);
|
|
1306
|
+
},
|
|
1307
|
+
configurable: true,
|
|
1308
|
+
enumerable: false
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
// ============================================================
|
|
1312
|
+
// Additional Parsing Functions
|
|
1313
|
+
// ============================================================
|
|
1314
|
+
|
|
1315
|
+
// Parse URL into components
|
|
1316
|
+
const parseUrl = (urlString) => {
|
|
1317
|
+
try {
|
|
1318
|
+
const url = new URL(urlString);
|
|
1319
|
+
const pathParts = url.pathname.split('/').filter(Boolean);
|
|
1320
|
+
return {
|
|
1321
|
+
protocol: url.protocol.replace(':', ''),
|
|
1322
|
+
host: url.host,
|
|
1323
|
+
pathname: url.pathname,
|
|
1324
|
+
path: pathParts,
|
|
1325
|
+
search: url.search,
|
|
1326
|
+
hash: url.hash,
|
|
1327
|
+
origin: url.origin
|
|
1328
|
+
};
|
|
1329
|
+
} catch {
|
|
1330
|
+
return { pathname: urlString, path: urlString.split('/').filter(Boolean) };
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
// ============================================================
|
|
1335
|
+
// Workflow/Event System (aligned with ai-workflows)
|
|
1336
|
+
// ============================================================
|
|
1337
|
+
|
|
1338
|
+
// Event handler registry
|
|
1339
|
+
const __event_handlers__ = new Map();
|
|
1340
|
+
// Schedule handler registry
|
|
1341
|
+
const __schedule_handlers__ = [];
|
|
1342
|
+
// Workflow history for tracking
|
|
1343
|
+
const __workflow_history__ = [];
|
|
1344
|
+
|
|
1345
|
+
// Known cron patterns for schedules
|
|
1346
|
+
const __KNOWN_PATTERNS__ = {
|
|
1347
|
+
second: '* * * * * *', minute: '* * * * *', hour: '0 * * * *',
|
|
1348
|
+
day: '0 0 * * *', week: '0 0 * * 0', month: '0 0 1 * *', year: '0 0 1 1 *',
|
|
1349
|
+
Monday: '0 0 * * 1', Tuesday: '0 0 * * 2', Wednesday: '0 0 * * 3',
|
|
1350
|
+
Thursday: '0 0 * * 4', Friday: '0 0 * * 5', Saturday: '0 0 * * 6', Sunday: '0 0 * * 0',
|
|
1351
|
+
weekday: '0 0 * * 1-5', weekend: '0 0 * * 0,6', midnight: '0 0 * * *', noon: '0 12 * * *'
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
// Time patterns for schedule modifiers
|
|
1355
|
+
const __TIME_PATTERNS__ = {
|
|
1356
|
+
at6am: { hour: 6, minute: 0 }, at7am: { hour: 7, minute: 0 }, at8am: { hour: 8, minute: 0 },
|
|
1357
|
+
at9am: { hour: 9, minute: 0 }, at10am: { hour: 10, minute: 0 }, at11am: { hour: 11, minute: 0 },
|
|
1358
|
+
at12pm: { hour: 12, minute: 0 }, atnoon: { hour: 12, minute: 0 }, at1pm: { hour: 13, minute: 0 },
|
|
1359
|
+
at2pm: { hour: 14, minute: 0 }, at3pm: { hour: 15, minute: 0 }, at4pm: { hour: 16, minute: 0 },
|
|
1360
|
+
at5pm: { hour: 17, minute: 0 }, at6pm: { hour: 18, minute: 0 }, at7pm: { hour: 19, minute: 0 },
|
|
1361
|
+
at8pm: { hour: 20, minute: 0 }, at9pm: { hour: 21, minute: 0 }, atmidnight: { hour: 0, minute: 0 }
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
// Parse event string (Noun.event)
|
|
1365
|
+
const __parseEvent__ = (event) => {
|
|
1366
|
+
const parts = event.split('.');
|
|
1367
|
+
if (parts.length !== 2) return null;
|
|
1368
|
+
return { noun: parts[0], event: parts[1] };
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
// Register event handler
|
|
1372
|
+
const __registerEventHandler__ = (noun, event, handler) => {
|
|
1373
|
+
const key = noun + '.' + event;
|
|
1374
|
+
if (!__event_handlers__.has(key)) __event_handlers__.set(key, []);
|
|
1375
|
+
__event_handlers__.get(key).push({ handler, source: handler.toString() });
|
|
1376
|
+
};
|
|
1377
|
+
|
|
1378
|
+
// Register schedule handler
|
|
1379
|
+
const __registerScheduleHandler__ = (interval, handler) => {
|
|
1380
|
+
__schedule_handlers__.push({ interval, handler, source: handler.toString() });
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
// Create $.on - supports both on('event', handler) and on.Entity.event(handler)
|
|
1384
|
+
const on = new Proxy(function(event, filterOrHandler, handler) {
|
|
1385
|
+
// on('user.created', handler) or on('user.created', { where: ... }, handler)
|
|
1386
|
+
if (typeof event === 'string') {
|
|
1387
|
+
const actualHandler = typeof filterOrHandler === 'function' ? filterOrHandler : handler;
|
|
1388
|
+
const filter = typeof filterOrHandler === 'object' ? filterOrHandler : null;
|
|
1389
|
+
const parts = event.split('.');
|
|
1390
|
+
const key = event; // Use full event string as key
|
|
1391
|
+
if (!__event_handlers__.has(key)) __event_handlers__.set(key, []);
|
|
1392
|
+
__event_handlers__.get(key).push({ handler: actualHandler, filter, source: actualHandler.toString() });
|
|
1393
|
+
return {
|
|
1394
|
+
off: () => {
|
|
1395
|
+
const handlers = __event_handlers__.get(key);
|
|
1396
|
+
if (handlers) {
|
|
1397
|
+
const idx = handlers.findIndex(h => h.handler === actualHandler);
|
|
1398
|
+
if (idx > -1) handlers.splice(idx, 1);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
}, {
|
|
1404
|
+
get: (target, prop) => {
|
|
1405
|
+
if (prop === 'once') {
|
|
1406
|
+
// on.once('event', handler) - one-time handler
|
|
1407
|
+
return (event, handler) => {
|
|
1408
|
+
const wrapper = async (data, $) => {
|
|
1409
|
+
const key = event;
|
|
1410
|
+
const handlers = __event_handlers__.get(key);
|
|
1411
|
+
if (handlers) {
|
|
1412
|
+
const idx = handlers.findIndex(h => h.handler === wrapper);
|
|
1413
|
+
if (idx > -1) handlers.splice(idx, 1);
|
|
1414
|
+
}
|
|
1415
|
+
return handler(data, $);
|
|
1416
|
+
};
|
|
1417
|
+
const key = event;
|
|
1418
|
+
if (!__event_handlers__.has(key)) __event_handlers__.set(key, []);
|
|
1419
|
+
__event_handlers__.get(key).push({ handler: wrapper, source: handler.toString() });
|
|
1420
|
+
return { off: () => {} };
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
// on.Entity.event(handler) pattern
|
|
1424
|
+
const noun = String(prop);
|
|
1425
|
+
return new Proxy({}, {
|
|
1426
|
+
get: (_, eventName) => (handler) => {
|
|
1427
|
+
const key = noun + '.' + String(eventName);
|
|
1428
|
+
__registerEventHandler__(noun, String(eventName), handler);
|
|
1429
|
+
return {
|
|
1430
|
+
off: () => {
|
|
1431
|
+
const handlers = __event_handlers__.get(key);
|
|
1432
|
+
if (handlers) {
|
|
1433
|
+
const idx = handlers.findIndex(h => h.handler === handler);
|
|
1434
|
+
if (idx > -1) handlers.splice(idx, 1);
|
|
1435
|
+
}
|
|
1436
|
+
},
|
|
1437
|
+
unsubscribe: () => {
|
|
1438
|
+
const handlers = __event_handlers__.get(key);
|
|
1439
|
+
if (handlers) {
|
|
1440
|
+
const idx = handlers.findIndex(h => h.handler === handler);
|
|
1441
|
+
if (idx > -1) handlers.splice(idx, 1);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
},
|
|
1448
|
+
apply: (target, thisArg, args) => target(...args)
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
// Parse duration string (100ms, 1s, 5m, etc.)
|
|
1452
|
+
const __parseDuration__ = (str) => {
|
|
1453
|
+
if (!str || typeof str !== 'string') return null;
|
|
1454
|
+
const match = str.match(/^(\\d+)(ms|s|m|h|d|w)?$/);
|
|
1455
|
+
if (!match) return null;
|
|
1456
|
+
const value = parseInt(match[1], 10);
|
|
1457
|
+
const unit = match[2] || 'ms';
|
|
1458
|
+
switch (unit) {
|
|
1459
|
+
case 'ms': return value;
|
|
1460
|
+
case 's': return value * 1000;
|
|
1461
|
+
case 'm': return value * 60 * 1000;
|
|
1462
|
+
case 'h': return value * 60 * 60 * 1000;
|
|
1463
|
+
case 'd': return value * 24 * 60 * 60 * 1000;
|
|
1464
|
+
case 'w': return value * 7 * 24 * 60 * 60 * 1000;
|
|
1465
|
+
default: return value;
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
// Check if string looks like a cron expression
|
|
1470
|
+
const __isCronExpression__ = (str) => {
|
|
1471
|
+
if (!str || typeof str !== 'string') return false;
|
|
1472
|
+
const parts = str.trim().split(/\\s+/);
|
|
1473
|
+
return parts.length >= 5 && parts.length <= 6;
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
// Active schedule timers
|
|
1477
|
+
const __schedule_timers__ = [];
|
|
1478
|
+
|
|
1479
|
+
// Create $.every - supports every('100ms', handler), every('name', '* * * *', handler), and every.Monday.at9am(handler)
|
|
1480
|
+
const every = new Proxy(function(intervalOrName, handlerOrCronOrOptions, handlerArg) {
|
|
1481
|
+
// Determine format: every('100ms', handler) or every('name', '* * * *', handler) or every('name', opts, handler)
|
|
1482
|
+
let name = null;
|
|
1483
|
+
let interval = null;
|
|
1484
|
+
let handler = null;
|
|
1485
|
+
let options = {};
|
|
1486
|
+
|
|
1487
|
+
if (typeof handlerOrCronOrOptions === 'function') {
|
|
1488
|
+
// every('100ms', handler) or every('* * * *', handler)
|
|
1489
|
+
interval = intervalOrName;
|
|
1490
|
+
handler = handlerOrCronOrOptions;
|
|
1491
|
+
} else if (typeof handlerArg === 'function') {
|
|
1492
|
+
// every('name', '* * * *', handler) or every('name', { immediate: true }, handler)
|
|
1493
|
+
name = intervalOrName;
|
|
1494
|
+
if (typeof handlerOrCronOrOptions === 'string') {
|
|
1495
|
+
interval = handlerOrCronOrOptions;
|
|
1496
|
+
} else {
|
|
1497
|
+
options = handlerOrCronOrOptions || {};
|
|
1498
|
+
interval = options.interval;
|
|
1499
|
+
}
|
|
1500
|
+
handler = handlerArg;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// Determine if it's a duration or cron
|
|
1504
|
+
const isCron = __isCronExpression__(interval);
|
|
1505
|
+
const durationMs = isCron ? null : __parseDuration__(interval);
|
|
1506
|
+
|
|
1507
|
+
let stopped = false;
|
|
1508
|
+
let timer = null;
|
|
1509
|
+
let runCount = 0;
|
|
1510
|
+
|
|
1511
|
+
const job = {
|
|
1512
|
+
name: name || interval,
|
|
1513
|
+
cron: isCron ? interval : null,
|
|
1514
|
+
stopped: false,
|
|
1515
|
+
stop: () => {
|
|
1516
|
+
stopped = true;
|
|
1517
|
+
job.stopped = true;
|
|
1518
|
+
if (timer) clearInterval(timer);
|
|
1519
|
+
},
|
|
1520
|
+
next: () => new Date(Date.now() + (durationMs || 60000))
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
if (durationMs) {
|
|
1524
|
+
// Duration-based schedule
|
|
1525
|
+
if (options.immediate) {
|
|
1526
|
+
Promise.resolve().then(() => handler());
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
timer = setInterval(async () => {
|
|
1530
|
+
if (stopped) return;
|
|
1531
|
+
if (options.until && options.until()) {
|
|
1532
|
+
job.stop();
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
runCount++;
|
|
1536
|
+
try {
|
|
1537
|
+
await handler();
|
|
1538
|
+
} catch (e) {
|
|
1539
|
+
console.error('[every] Handler error:', e);
|
|
1540
|
+
}
|
|
1541
|
+
}, durationMs);
|
|
1542
|
+
|
|
1543
|
+
__schedule_timers__.push(timer);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
__registerScheduleHandler__({
|
|
1547
|
+
type: isCron ? 'cron' : 'duration',
|
|
1548
|
+
expression: isCron ? interval : null,
|
|
1549
|
+
ms: durationMs,
|
|
1550
|
+
natural: name || interval
|
|
1551
|
+
}, handler);
|
|
1552
|
+
|
|
1553
|
+
return job;
|
|
1554
|
+
}, {
|
|
1555
|
+
get: (target, prop) => {
|
|
1556
|
+
const propStr = String(prop);
|
|
1557
|
+
const pattern = __KNOWN_PATTERNS__[propStr];
|
|
1558
|
+
if (pattern) {
|
|
1559
|
+
const result = (handler) => {
|
|
1560
|
+
__registerScheduleHandler__({ type: 'cron', expression: pattern, natural: propStr }, handler);
|
|
1561
|
+
return { stop: () => {}, cancel: () => {}, name: propStr, cron: pattern, stopped: false };
|
|
1562
|
+
};
|
|
1563
|
+
// Support time modifiers: every.Monday.at9am(handler)
|
|
1564
|
+
return new Proxy(result, {
|
|
1565
|
+
get: (_, timeKey) => {
|
|
1566
|
+
const time = __TIME_PATTERNS__[String(timeKey)];
|
|
1567
|
+
if (time) {
|
|
1568
|
+
const parts = pattern.split(' ');
|
|
1569
|
+
parts[0] = String(time.minute);
|
|
1570
|
+
parts[1] = String(time.hour);
|
|
1571
|
+
const cron = parts.join(' ');
|
|
1572
|
+
return (handler) => {
|
|
1573
|
+
__registerScheduleHandler__({ type: 'cron', expression: cron, natural: propStr + '.' + String(timeKey) }, handler);
|
|
1574
|
+
return { stop: () => {}, cancel: () => {}, name: propStr + '.' + String(timeKey), cron, stopped: false };
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
return undefined;
|
|
1578
|
+
},
|
|
1579
|
+
apply: (_, __, args) => {
|
|
1580
|
+
__registerScheduleHandler__({ type: 'cron', expression: pattern, natural: propStr }, args[0]);
|
|
1581
|
+
return { stop: () => {}, cancel: () => {}, name: propStr, cron: pattern, stopped: false };
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
// Plural units: every.seconds(5), every.minutes(10), etc.
|
|
1586
|
+
const pluralUnits = { seconds: 'second', minutes: 'minute', hours: 'hour', days: 'day', weeks: 'week' };
|
|
1587
|
+
if (pluralUnits[propStr]) {
|
|
1588
|
+
return (value) => (handler) => {
|
|
1589
|
+
__registerScheduleHandler__({ type: pluralUnits[propStr], value, natural: value + ' ' + propStr }, handler);
|
|
1590
|
+
return { stop: () => {}, cancel: () => {}, name: value + ' ' + propStr, stopped: false };
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
return undefined;
|
|
1594
|
+
},
|
|
1595
|
+
apply: (target, thisArg, args) => target(...args)
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
// Send event - returns event info, supports options (delay, correlationId, channel, wait)
|
|
1599
|
+
const send = async (event, data, options = {}) => {
|
|
1600
|
+
const eventId = __generateId__();
|
|
1601
|
+
const timestamp = Date.now();
|
|
1602
|
+
const eventObj = {
|
|
1603
|
+
id: eventId,
|
|
1604
|
+
type: event,
|
|
1605
|
+
data,
|
|
1606
|
+
timestamp,
|
|
1607
|
+
correlationId: options.correlationId
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
__workflow_history__.push({ type: 'event', name: event, data, timestamp });
|
|
1611
|
+
|
|
1612
|
+
// Handle delay
|
|
1613
|
+
if (options.delay) {
|
|
1614
|
+
const delayMs = typeof options.delay === 'string' ? __parseDuration__(options.delay) : options.delay;
|
|
1615
|
+
if (delayMs) {
|
|
1616
|
+
await new Promise(r => setTimeout(r, delayMs));
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// Find matching handlers (supports wildcard like 'user.*')
|
|
1621
|
+
const matchingHandlers = [];
|
|
1622
|
+
for (const [key, handlers] of __event_handlers__) {
|
|
1623
|
+
const keyPattern = key.replace(/\\.\\*/g, '\\\\.[^.]+');
|
|
1624
|
+
const regex = new RegExp('^' + keyPattern + '$');
|
|
1625
|
+
if (key === event || regex.test(event) || (key.endsWith('.*') && event.startsWith(key.slice(0, -1)))) {
|
|
1626
|
+
for (const h of handlers) {
|
|
1627
|
+
// Check channel filter
|
|
1628
|
+
if (h.filter?.channel && h.filter.channel !== options.channel) continue;
|
|
1629
|
+
// Check where filter
|
|
1630
|
+
if (h.filter?.where) {
|
|
1631
|
+
let match = true;
|
|
1632
|
+
for (const [k, v] of Object.entries(h.filter.where)) {
|
|
1633
|
+
if (data[k] !== v) { match = false; break; }
|
|
1634
|
+
}
|
|
1635
|
+
if (!match) continue;
|
|
1636
|
+
}
|
|
1637
|
+
matchingHandlers.push(h);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// If wait option, call first handler and return response
|
|
1643
|
+
if (options.wait && matchingHandlers.length > 0) {
|
|
1644
|
+
let response = null;
|
|
1645
|
+
const reply = (data) => { response = { data }; };
|
|
1646
|
+
await matchingHandlers[0].handler({ type: event, data, correlationId: options.correlationId }, reply);
|
|
1647
|
+
return response || eventObj;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// Fire all handlers
|
|
1651
|
+
await Promise.all(matchingHandlers.map(async ({ handler }) => {
|
|
1652
|
+
try {
|
|
1653
|
+
await handler({ type: event, data, correlationId: options.correlationId }, $);
|
|
1654
|
+
} catch (error) {
|
|
1655
|
+
console.error('Error in handler for ' + event + ':', error);
|
|
1656
|
+
}
|
|
1657
|
+
}));
|
|
1658
|
+
|
|
1659
|
+
return eventObj;
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
// Add broadcast method to send
|
|
1663
|
+
send.broadcast = async (event, data) => {
|
|
1664
|
+
return send(event, data);
|
|
1665
|
+
};
|
|
1666
|
+
|
|
1667
|
+
// Delay helper
|
|
1668
|
+
const delay = (ms) => {
|
|
1669
|
+
if (typeof ms === 'string') ms = __parseDuration__(ms) || 0;
|
|
1670
|
+
return new Promise(r => setTimeout(r, ms));
|
|
1671
|
+
};
|
|
1672
|
+
|
|
1673
|
+
// Decide helper - pattern matching decision
|
|
1674
|
+
const decide = (subject) => {
|
|
1675
|
+
const conditions = [];
|
|
1676
|
+
let defaultValue = null;
|
|
1677
|
+
|
|
1678
|
+
const chain = {
|
|
1679
|
+
when: (conditionOrFn, result) => {
|
|
1680
|
+
conditions.push({ condition: conditionOrFn, result });
|
|
1681
|
+
return chain;
|
|
1682
|
+
},
|
|
1683
|
+
otherwise: (result) => {
|
|
1684
|
+
defaultValue = result;
|
|
1685
|
+
// Execute decision
|
|
1686
|
+
for (const { condition, result } of conditions) {
|
|
1687
|
+
let matches = false;
|
|
1688
|
+
if (typeof condition === 'function') {
|
|
1689
|
+
matches = condition(subject);
|
|
1690
|
+
} else if (typeof condition === 'object') {
|
|
1691
|
+
matches = true;
|
|
1692
|
+
for (const [key, value] of Object.entries(condition)) {
|
|
1693
|
+
const subjectValue = subject[key];
|
|
1694
|
+
if (typeof value === 'object' && value !== null) {
|
|
1695
|
+
for (const [op, opVal] of Object.entries(value)) {
|
|
1696
|
+
switch (op) {
|
|
1697
|
+
case '$gte': if (!(subjectValue >= opVal)) matches = false; break;
|
|
1698
|
+
case '$gt': if (!(subjectValue > opVal)) matches = false; break;
|
|
1699
|
+
case '$lte': if (!(subjectValue <= opVal)) matches = false; break;
|
|
1700
|
+
case '$lt': if (!(subjectValue < opVal)) matches = false; break;
|
|
1701
|
+
case '$ne': if (subjectValue === opVal) matches = false; break;
|
|
1702
|
+
default: if (subjectValue !== value) matches = false;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
} else if (subjectValue !== value) {
|
|
1706
|
+
matches = false;
|
|
1707
|
+
}
|
|
1708
|
+
if (!matches) break;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
if (matches) {
|
|
1712
|
+
return typeof result === 'function' ? result() : result;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return typeof defaultValue === 'function' ? defaultValue() : defaultValue;
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
|
|
1719
|
+
return chain;
|
|
1720
|
+
};
|
|
1721
|
+
|
|
1722
|
+
// Async decide
|
|
1723
|
+
decide.async = (subject) => {
|
|
1724
|
+
const conditions = [];
|
|
1725
|
+
|
|
1726
|
+
const chain = {
|
|
1727
|
+
when: (conditionFn, result) => {
|
|
1728
|
+
conditions.push({ condition: conditionFn, result });
|
|
1729
|
+
return chain;
|
|
1730
|
+
},
|
|
1731
|
+
otherwise: async (defaultResult) => {
|
|
1732
|
+
for (const { condition, result } of conditions) {
|
|
1733
|
+
const matches = await condition(subject);
|
|
1734
|
+
if (matches) return result;
|
|
1735
|
+
}
|
|
1736
|
+
return defaultResult;
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
|
|
1740
|
+
return chain;
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1743
|
+
// Track helper - analytics tracking
|
|
1744
|
+
const __tracked_events__ = [];
|
|
1745
|
+
const track = async (event, data, metadata = {}) => {
|
|
1746
|
+
const entry = { type: event, data, metadata, timestamp: Date.now(), userId: metadata.userId };
|
|
1747
|
+
__tracked_events__.push(entry);
|
|
1748
|
+
return entry;
|
|
1749
|
+
};
|
|
1750
|
+
|
|
1751
|
+
track.user = async (userId, event, data) => {
|
|
1752
|
+
const entry = { type: event, data, userId, timestamp: Date.now() };
|
|
1753
|
+
__tracked_events__.push(entry);
|
|
1754
|
+
return entry;
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
track.query = async (options = {}) => {
|
|
1758
|
+
let results = [...__tracked_events__];
|
|
1759
|
+
if (options.type) results = results.filter(e => e.type === options.type);
|
|
1760
|
+
if (options.userId) results = results.filter(e => e.userId === options.userId);
|
|
1761
|
+
if (options['data.buttonId']) results = results.filter(e => e.data?.buttonId === options['data.buttonId']);
|
|
1762
|
+
if (options.orderBy) {
|
|
1763
|
+
results.sort((a, b) => {
|
|
1764
|
+
const aVal = a[options.orderBy];
|
|
1765
|
+
const bVal = b[options.orderBy];
|
|
1766
|
+
return options.order === 'desc' ? bVal - aVal : aVal - bVal;
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
if (options.limit) results = results.slice(0, options.limit);
|
|
1770
|
+
return results;
|
|
1771
|
+
};
|
|
1772
|
+
|
|
1773
|
+
track.funnel = async (steps, options = {}) => {
|
|
1774
|
+
const events = await track.query(options);
|
|
1775
|
+
const completed = steps.every(step => events.some(e => e.type === step));
|
|
1776
|
+
return {
|
|
1777
|
+
steps: steps.map(step => ({ step, completed: events.some(e => e.type === step) })),
|
|
1778
|
+
completed,
|
|
1779
|
+
conversionRate: completed ? 1 : 0
|
|
1780
|
+
};
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1783
|
+
track.aggregate = async (event, options = {}) => {
|
|
1784
|
+
const events = __tracked_events__.filter(e => e.type === event);
|
|
1785
|
+
const groups = {};
|
|
1786
|
+
for (const e of events) {
|
|
1787
|
+
const key = options.groupBy ? e.data[options.groupBy] : '_all';
|
|
1788
|
+
if (!groups[key]) groups[key] = { sum: 0, count: 0, values: [] };
|
|
1789
|
+
groups[key].count++;
|
|
1790
|
+
if (options.sum) groups[key].sum += e.data[options.sum] || 0;
|
|
1791
|
+
groups[key].values.push(e);
|
|
1792
|
+
}
|
|
1793
|
+
return groups;
|
|
1794
|
+
};
|
|
1795
|
+
|
|
1796
|
+
track.timeseries = async (event, options = {}) => {
|
|
1797
|
+
const events = __tracked_events__.filter(e => e.type === event);
|
|
1798
|
+
return events.map(e => ({
|
|
1799
|
+
timestamp: e.timestamp,
|
|
1800
|
+
value: options.field ? e.data[options.field] : 1
|
|
1801
|
+
}));
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
// Experiment helper - A/B testing
|
|
1805
|
+
const experiment = (name) => {
|
|
1806
|
+
const variants = [];
|
|
1807
|
+
let eligibilityFn = null;
|
|
1808
|
+
const overrides = new Map();
|
|
1809
|
+
let rolloutConfig = null;
|
|
1810
|
+
|
|
1811
|
+
const exp = {
|
|
1812
|
+
variant: (variantName, config, options = {}) => {
|
|
1813
|
+
variants.push({ name: variantName, config, weight: options.weight || 1 });
|
|
1814
|
+
return exp;
|
|
1815
|
+
},
|
|
1816
|
+
eligible: (fn) => {
|
|
1817
|
+
eligibilityFn = fn;
|
|
1818
|
+
return exp;
|
|
1819
|
+
},
|
|
1820
|
+
override: (userId, variantName) => {
|
|
1821
|
+
overrides.set(userId, variantName);
|
|
1822
|
+
return exp;
|
|
1823
|
+
},
|
|
1824
|
+
rollout: (variantName, percentage) => {
|
|
1825
|
+
rolloutConfig = { variant: variantName, percentage };
|
|
1826
|
+
return exp;
|
|
1827
|
+
},
|
|
1828
|
+
assign: (userId) => {
|
|
1829
|
+
// Check override
|
|
1830
|
+
if (overrides.has(userId)) {
|
|
1831
|
+
const overrideVariant = variants.find(v => v.name === overrides.get(userId));
|
|
1832
|
+
if (overrideVariant) return { name: overrideVariant.name, ...overrideVariant.config };
|
|
1833
|
+
}
|
|
1834
|
+
// Check eligibility
|
|
1835
|
+
if (eligibilityFn && typeof userId === 'object' && !eligibilityFn(userId)) {
|
|
1836
|
+
return variants[0] ? { name: variants[0].name, ...variants[0].config } : {};
|
|
1837
|
+
}
|
|
1838
|
+
// Consistent assignment based on userId hash
|
|
1839
|
+
const userIdStr = typeof userId === 'object' ? userId.id || JSON.stringify(userId) : String(userId);
|
|
1840
|
+
let hash = 0;
|
|
1841
|
+
for (let i = 0; i < userIdStr.length; i++) {
|
|
1842
|
+
hash = ((hash << 5) - hash) + userIdStr.charCodeAt(i);
|
|
1843
|
+
hash = hash & hash;
|
|
1844
|
+
}
|
|
1845
|
+
const normalized = Math.abs(hash % 100) / 100;
|
|
1846
|
+
|
|
1847
|
+
// Check rollout
|
|
1848
|
+
if (rolloutConfig && normalized < rolloutConfig.percentage) {
|
|
1849
|
+
const rolloutVariant = variants.find(v => v.name === rolloutConfig.variant);
|
|
1850
|
+
if (rolloutVariant) return { name: rolloutVariant.name, ...rolloutVariant.config };
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// Weight-based selection
|
|
1854
|
+
const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0);
|
|
1855
|
+
let cumulative = 0;
|
|
1856
|
+
for (const v of variants) {
|
|
1857
|
+
cumulative += v.weight / totalWeight;
|
|
1858
|
+
if (normalized < cumulative) return { name: v.name, ...v.config };
|
|
1859
|
+
}
|
|
1860
|
+
return variants[0] ? { name: variants[0].name, ...variants[0].config } : {};
|
|
1861
|
+
},
|
|
1862
|
+
track: {
|
|
1863
|
+
exposure: async (userId) => {
|
|
1864
|
+
await track('experiment.exposure', { experiment: name, userId });
|
|
1865
|
+
},
|
|
1866
|
+
conversion: async (userId, data = {}) => {
|
|
1867
|
+
await track('experiment.conversion', { experiment: name, userId, ...data });
|
|
1868
|
+
}
|
|
1869
|
+
},
|
|
1870
|
+
results: async () => {
|
|
1871
|
+
const exposures = __tracked_events__.filter(e => e.type === 'experiment.exposure' && e.data.experiment === name);
|
|
1872
|
+
const conversions = __tracked_events__.filter(e => e.type === 'experiment.conversion' && e.data.experiment === name);
|
|
1873
|
+
const result = {};
|
|
1874
|
+
for (const v of variants) {
|
|
1875
|
+
result[v.name] = { exposures: 0, conversions: 0 };
|
|
1876
|
+
}
|
|
1877
|
+
for (const e of exposures) {
|
|
1878
|
+
const variantName = exp.assign(e.data.userId).name;
|
|
1879
|
+
if (result[variantName]) result[variantName].exposures++;
|
|
1880
|
+
}
|
|
1881
|
+
for (const e of conversions) {
|
|
1882
|
+
const variantName = exp.assign(e.data.userId).name;
|
|
1883
|
+
if (result[variantName]) result[variantName].conversions++;
|
|
1884
|
+
}
|
|
1885
|
+
return result;
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
|
|
1889
|
+
return exp;
|
|
1890
|
+
};
|
|
1891
|
+
|
|
1892
|
+
// Execute event and wait for result ($.do) - mirrors ai-workflows do
|
|
1893
|
+
const __doEvent__ = async (event, data) => {
|
|
1894
|
+
__workflow_history__.push({ type: 'action', name: 'do:' + event, data, timestamp: Date.now() });
|
|
1895
|
+
|
|
1896
|
+
const parsed = __parseEvent__(event);
|
|
1897
|
+
if (!parsed) throw new Error('Invalid event format: ' + event + '. Expected Noun.event');
|
|
1898
|
+
|
|
1899
|
+
const key = parsed.noun + '.' + parsed.event;
|
|
1900
|
+
const handlers = __event_handlers__.get(key) || [];
|
|
1901
|
+
if (handlers.length === 0) throw new Error('No handler registered for ' + event);
|
|
1902
|
+
|
|
1903
|
+
return await handlers[0].handler(data, $);
|
|
1904
|
+
};
|
|
1905
|
+
|
|
1906
|
+
// Try event (non-durable) - mirrors ai-workflows try
|
|
1907
|
+
const __tryEvent__ = async (event, data) => {
|
|
1908
|
+
__workflow_history__.push({ type: 'action', name: 'try:' + event, data, timestamp: Date.now() });
|
|
1909
|
+
|
|
1910
|
+
const parsed = __parseEvent__(event);
|
|
1911
|
+
if (!parsed) throw new Error('Invalid event format: ' + event + '. Expected Noun.event');
|
|
1912
|
+
|
|
1913
|
+
const key = parsed.noun + '.' + parsed.event;
|
|
1914
|
+
const handlers = __event_handlers__.get(key) || [];
|
|
1915
|
+
if (handlers.length === 0) throw new Error('No handler registered for ' + event);
|
|
1916
|
+
|
|
1917
|
+
return await handlers[0].handler(data, $);
|
|
1918
|
+
};
|
|
1919
|
+
|
|
1920
|
+
// Queue implementation
|
|
1921
|
+
const __queues__ = new Map();
|
|
1922
|
+
const __queue_stats__ = new Map();
|
|
1923
|
+
const queue = (name) => {
|
|
1924
|
+
if (!__queues__.has(name)) {
|
|
1925
|
+
__queues__.set(name, []);
|
|
1926
|
+
__queue_stats__.set(name, { added: 0, processed: 0, failed: 0, retried: 0 });
|
|
1927
|
+
}
|
|
1928
|
+
const q = __queues__.get(name);
|
|
1929
|
+
const stats = __queue_stats__.get(name);
|
|
1930
|
+
return {
|
|
1931
|
+
add: async (item, options = {}) => {
|
|
1932
|
+
const job = { id: __generateId__(), item, options, addedAt: new Date(), priority: options.priority || 'normal', attempts: 0 };
|
|
1933
|
+
q.push(job);
|
|
1934
|
+
stats.added++;
|
|
1935
|
+
return job;
|
|
1936
|
+
},
|
|
1937
|
+
process: async (handler) => {
|
|
1938
|
+
while (q.length) {
|
|
1939
|
+
const job = q.shift();
|
|
1940
|
+
try {
|
|
1941
|
+
job.attempts++;
|
|
1942
|
+
await handler(job.item);
|
|
1943
|
+
stats.processed++;
|
|
1944
|
+
} catch (e) {
|
|
1945
|
+
stats.failed++;
|
|
1946
|
+
if (job.attempts < (job.options.maxRetries || 3)) {
|
|
1947
|
+
q.push(job); // Retry
|
|
1948
|
+
stats.retried++;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
},
|
|
1953
|
+
processBatch: async (handler, batchSize = 10) => {
|
|
1954
|
+
const batch = q.splice(0, batchSize);
|
|
1955
|
+
if (batch.length === 0) return;
|
|
1956
|
+
try {
|
|
1957
|
+
await handler(batch.map(j => j.item));
|
|
1958
|
+
stats.processed += batch.length;
|
|
1959
|
+
} catch (e) {
|
|
1960
|
+
stats.failed += batch.length;
|
|
1961
|
+
// Put back for retry
|
|
1962
|
+
for (const job of batch) {
|
|
1963
|
+
if (job.attempts < (job.options.maxRetries || 3)) {
|
|
1964
|
+
job.attempts++;
|
|
1965
|
+
q.push(job);
|
|
1966
|
+
stats.retried++;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
},
|
|
1971
|
+
size: () => q.length,
|
|
1972
|
+
clear: () => { q.length = 0; },
|
|
1973
|
+
stats: () => ({ ...stats, pending: q.length }),
|
|
1974
|
+
peek: (n = 1) => q.slice(0, n).map(j => j.item),
|
|
1975
|
+
getByPriority: (priority) => q.filter(j => j.priority === priority).map(j => j.item)
|
|
1976
|
+
};
|
|
1977
|
+
};
|
|
1978
|
+
|
|
1979
|
+
// Actor pattern implementation
|
|
1980
|
+
const __actors__ = new Map();
|
|
1981
|
+
const actor = (type) => ({
|
|
1982
|
+
register: (id, state = {}) => {
|
|
1983
|
+
const key = type + ':' + id;
|
|
1984
|
+
__actors__.set(key, { id, type, state, createdAt: new Date() });
|
|
1985
|
+
return __actors__.get(key);
|
|
1986
|
+
},
|
|
1987
|
+
get: (id) => __actors__.get(type + ':' + id),
|
|
1988
|
+
send: async (id, message) => {
|
|
1989
|
+
const a = __actors__.get(type + ':' + id);
|
|
1990
|
+
if (!a) throw new Error('Actor not found: ' + type + ':' + id);
|
|
1991
|
+
// Handle message (stub - in real impl would dispatch to actor handler)
|
|
1992
|
+
return { delivered: true };
|
|
1993
|
+
}
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
// Actions API (workflow actions)
|
|
1997
|
+
const __actions__ = new Map();
|
|
1998
|
+
const actions = {
|
|
1999
|
+
async create(options) {
|
|
2000
|
+
const id = __generateId__();
|
|
2001
|
+
const action = {
|
|
2002
|
+
id,
|
|
2003
|
+
actor: options.actor,
|
|
2004
|
+
object: options.object,
|
|
2005
|
+
action: options.action,
|
|
2006
|
+
metadata: options.metadata || {},
|
|
2007
|
+
status: 'pending',
|
|
2008
|
+
createdAt: new Date(),
|
|
2009
|
+
updatedAt: new Date()
|
|
2010
|
+
};
|
|
2011
|
+
__actions__.set(id, action);
|
|
2012
|
+
return action;
|
|
2013
|
+
},
|
|
2014
|
+
async get(id) {
|
|
2015
|
+
return __actions__.get(id) || null;
|
|
2016
|
+
},
|
|
2017
|
+
async start(id) {
|
|
2018
|
+
const action = __actions__.get(id);
|
|
2019
|
+
if (!action) throw new Error('Action not found: ' + id);
|
|
2020
|
+
action.status = 'active';
|
|
2021
|
+
action.startedAt = new Date();
|
|
2022
|
+
action.updatedAt = new Date();
|
|
2023
|
+
return action;
|
|
2024
|
+
},
|
|
2025
|
+
async complete(id, result = {}) {
|
|
2026
|
+
const action = __actions__.get(id);
|
|
2027
|
+
if (!action) throw new Error('Action not found: ' + id);
|
|
2028
|
+
action.status = 'completed';
|
|
2029
|
+
action.result = result;
|
|
2030
|
+
action.completedAt = new Date();
|
|
2031
|
+
action.updatedAt = new Date();
|
|
2032
|
+
return action;
|
|
2033
|
+
},
|
|
2034
|
+
async fail(id, error) {
|
|
2035
|
+
const action = __actions__.get(id);
|
|
2036
|
+
if (!action) throw new Error('Action not found: ' + id);
|
|
2037
|
+
action.status = 'failed';
|
|
2038
|
+
action.error = error;
|
|
2039
|
+
action.failedAt = new Date();
|
|
2040
|
+
action.updatedAt = new Date();
|
|
2041
|
+
return action;
|
|
2042
|
+
},
|
|
2043
|
+
async list(options = {}) {
|
|
2044
|
+
let results = Array.from(__actions__.values());
|
|
2045
|
+
if (options.status) results = results.filter(a => a.status === options.status);
|
|
2046
|
+
if (options.actor) results = results.filter(a => a.actor === options.actor);
|
|
2047
|
+
if (options.limit) results = results.slice(0, options.limit);
|
|
2048
|
+
return results;
|
|
2049
|
+
},
|
|
2050
|
+
async retry(id) {
|
|
2051
|
+
const action = __actions__.get(id);
|
|
2052
|
+
if (!action) throw new Error('Action not found: ' + id);
|
|
2053
|
+
action.status = 'pending';
|
|
2054
|
+
action.retryCount = (action.retryCount || 0) + 1;
|
|
2055
|
+
action.updatedAt = new Date();
|
|
2056
|
+
return action;
|
|
2057
|
+
},
|
|
2058
|
+
async cancel(id) {
|
|
2059
|
+
const action = __actions__.get(id);
|
|
2060
|
+
if (!action) throw new Error('Action not found: ' + id);
|
|
2061
|
+
action.status = 'cancelled';
|
|
2062
|
+
action.cancelledAt = new Date();
|
|
2063
|
+
action.updatedAt = new Date();
|
|
2064
|
+
return action;
|
|
2065
|
+
}
|
|
2066
|
+
};
|
|
2067
|
+
|
|
2068
|
+
// Artifacts API (for storing results)
|
|
2069
|
+
const __artifacts__ = new Map();
|
|
2070
|
+
const artifacts = {
|
|
2071
|
+
async store(options) {
|
|
2072
|
+
const id = __generateId__();
|
|
2073
|
+
const artifact = {
|
|
2074
|
+
id,
|
|
2075
|
+
type: options.type,
|
|
2076
|
+
data: options.data,
|
|
2077
|
+
metadata: options.metadata || {},
|
|
2078
|
+
createdAt: new Date()
|
|
2079
|
+
};
|
|
2080
|
+
__artifacts__.set(id, artifact);
|
|
2081
|
+
return artifact;
|
|
2082
|
+
},
|
|
2083
|
+
async get(id) {
|
|
2084
|
+
return __artifacts__.get(id) || null;
|
|
2085
|
+
},
|
|
2086
|
+
async list(options = {}) {
|
|
2087
|
+
let results = Array.from(__artifacts__.values());
|
|
2088
|
+
if (options.type) results = results.filter(a => a.type === options.type);
|
|
2089
|
+
if (options.limit) results = results.slice(0, options.limit);
|
|
2090
|
+
return results;
|
|
2091
|
+
}
|
|
2092
|
+
};
|
|
2093
|
+
|
|
2094
|
+
// Events API (for event sourcing)
|
|
2095
|
+
const __events__ = [];
|
|
2096
|
+
const events = {
|
|
2097
|
+
async record(options) {
|
|
2098
|
+
const event = {
|
|
2099
|
+
id: __generateId__(),
|
|
2100
|
+
type: options.type,
|
|
2101
|
+
subject: options.subject,
|
|
2102
|
+
data: options.data,
|
|
2103
|
+
metadata: options.metadata || {},
|
|
2104
|
+
timestamp: new Date()
|
|
2105
|
+
};
|
|
2106
|
+
__events__.push(event);
|
|
2107
|
+
return event;
|
|
2108
|
+
},
|
|
2109
|
+
async list(options = {}) {
|
|
2110
|
+
let results = [...__events__];
|
|
2111
|
+
if (options.type) results = results.filter(e => e.type === options.type);
|
|
2112
|
+
if (options.subject) results = results.filter(e => e.subject === options.subject);
|
|
2113
|
+
if (options.limit) results = results.slice(0, options.limit);
|
|
2114
|
+
return results;
|
|
2115
|
+
},
|
|
2116
|
+
// Query is an alias for list with more flexible filtering
|
|
2117
|
+
async query(options = {}) {
|
|
2118
|
+
let results = [...__events__];
|
|
2119
|
+
if (options.type) results = results.filter(e => e.type === options.type);
|
|
2120
|
+
if (options.subject) results = results.filter(e => e.subject === options.subject);
|
|
2121
|
+
if (options.actor) results = results.filter(e => e.metadata?.actor === options.actor);
|
|
2122
|
+
if (options.from) results = results.filter(e => new Date(e.timestamp) >= new Date(options.from));
|
|
2123
|
+
if (options.to) results = results.filter(e => new Date(e.timestamp) <= new Date(options.to));
|
|
2124
|
+
if (options.orderBy === 'timestamp') {
|
|
2125
|
+
results.sort((a, b) => options.order === 'desc' ? b.timestamp - a.timestamp : a.timestamp - b.timestamp);
|
|
2126
|
+
}
|
|
2127
|
+
if (options.limit) results = results.slice(0, options.limit);
|
|
2128
|
+
return results;
|
|
2129
|
+
},
|
|
2130
|
+
async replay(handler, options = {}) {
|
|
2131
|
+
const evts = await events.list(options);
|
|
2132
|
+
for (const evt of evts) {
|
|
2133
|
+
await handler(evt);
|
|
2134
|
+
}
|
|
2135
|
+
},
|
|
2136
|
+
// Subscribe to events
|
|
2137
|
+
subscribe(type, handler) {
|
|
2138
|
+
on(type, handler);
|
|
2139
|
+
return { unsubscribe: () => {} };
|
|
2140
|
+
},
|
|
2141
|
+
// Emit an event (alias for record + send)
|
|
2142
|
+
async emit(type, data, metadata = {}) {
|
|
2143
|
+
const event = await events.record({ type, data, metadata });
|
|
2144
|
+
await send(type, data);
|
|
2145
|
+
return event;
|
|
2146
|
+
}
|
|
2147
|
+
};
|
|
2148
|
+
|
|
2149
|
+
// MDX Parsing helpers (basic implementation)
|
|
2150
|
+
const parse = (content) => {
|
|
2151
|
+
const frontmatterMatch = content.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/);
|
|
2152
|
+
if (!frontmatterMatch) {
|
|
2153
|
+
return { data: {}, content: content.trim(), type: null, id: null };
|
|
2154
|
+
}
|
|
2155
|
+
const [, frontmatter, body] = frontmatterMatch;
|
|
2156
|
+
const data = {};
|
|
2157
|
+
let currentKey = null;
|
|
2158
|
+
let inArray = false;
|
|
2159
|
+
const lines = frontmatter.split('\\n');
|
|
2160
|
+
for (const line of lines) {
|
|
2161
|
+
const keyMatch = line.match(/^(\\w+):\\s*(.*)$/);
|
|
2162
|
+
if (keyMatch) {
|
|
2163
|
+
currentKey = keyMatch[1];
|
|
2164
|
+
const value = keyMatch[2].trim();
|
|
2165
|
+
if (value === '') {
|
|
2166
|
+
data[currentKey] = [];
|
|
2167
|
+
inArray = true;
|
|
2168
|
+
} else {
|
|
2169
|
+
data[currentKey] = value;
|
|
2170
|
+
inArray = false;
|
|
2171
|
+
}
|
|
2172
|
+
} else if (inArray && currentKey && line.match(/^\\s+-\\s+(.+)$/)) {
|
|
2173
|
+
const itemMatch = line.match(/^\\s+-\\s+(.+)$/);
|
|
2174
|
+
if (itemMatch) {
|
|
2175
|
+
if (!Array.isArray(data[currentKey])) data[currentKey] = [];
|
|
2176
|
+
data[currentKey].push(itemMatch[1].trim());
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
return {
|
|
2181
|
+
data,
|
|
2182
|
+
content: body.trim(),
|
|
2183
|
+
type: data.$type || data['@type'] || null,
|
|
2184
|
+
id: data.$id || data['@id'] || null
|
|
2185
|
+
};
|
|
2186
|
+
};
|
|
2187
|
+
|
|
2188
|
+
const stringify = (doc) => {
|
|
2189
|
+
const lines = ['---'];
|
|
2190
|
+
if (doc.type) lines.push('$type: ' + doc.type);
|
|
2191
|
+
if (doc.id) lines.push('$id: ' + doc.id);
|
|
2192
|
+
for (const [key, value] of Object.entries(doc.data || {})) {
|
|
2193
|
+
if (Array.isArray(value)) {
|
|
2194
|
+
lines.push(key + ':');
|
|
2195
|
+
for (const item of value) lines.push(' - ' + item);
|
|
2196
|
+
} else {
|
|
2197
|
+
lines.push(key + ': ' + value);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
lines.push('---');
|
|
2201
|
+
if (doc.content) lines.push('', doc.content);
|
|
2202
|
+
return lines.join('\\n');
|
|
2203
|
+
};
|
|
2204
|
+
|
|
2205
|
+
const toAst = (doc) => {
|
|
2206
|
+
const children = [];
|
|
2207
|
+
const lines = (doc.content || '').split('\\n');
|
|
2208
|
+
for (const line of lines) {
|
|
2209
|
+
if (line.startsWith('# ')) children.push({ type: 'heading', depth: 1, text: line.slice(2) });
|
|
2210
|
+
else if (line.startsWith('## ')) children.push({ type: 'heading', depth: 2, text: line.slice(3) });
|
|
2211
|
+
else if (line.startsWith('- ')) {
|
|
2212
|
+
const lastList = children[children.length - 1];
|
|
2213
|
+
if (lastList?.type === 'list') {
|
|
2214
|
+
lastList.items.push(line.slice(2));
|
|
2215
|
+
} else {
|
|
2216
|
+
children.push({ type: 'list', items: [line.slice(2)] });
|
|
2217
|
+
}
|
|
2218
|
+
} else if (line.startsWith('\`\`\`')) {
|
|
2219
|
+
const lang = line.slice(3).trim();
|
|
2220
|
+
children.push({ type: 'code', lang: lang || null, value: '' });
|
|
2221
|
+
} else if (line.trim()) {
|
|
2222
|
+
children.push({ type: 'paragraph', text: line });
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
return { type: 'root', children };
|
|
2226
|
+
};
|
|
2227
|
+
|
|
2228
|
+
const renderMarkdown = (doc, options = {}) => {
|
|
2229
|
+
let result = '';
|
|
2230
|
+
if (options.includeFrontmatter && doc.data && Object.keys(doc.data).length > 0) {
|
|
2231
|
+
result += '---\\n';
|
|
2232
|
+
if (doc.type) result += '$type: ' + doc.type + '\\n';
|
|
2233
|
+
for (const [key, value] of Object.entries(doc.data)) {
|
|
2234
|
+
if (Array.isArray(value)) {
|
|
2235
|
+
result += key + ':\\n';
|
|
2236
|
+
for (const item of value) result += ' - ' + item + '\\n';
|
|
2237
|
+
} else {
|
|
2238
|
+
result += key + ': ' + value + '\\n';
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
result += '---\\n';
|
|
2242
|
+
}
|
|
2243
|
+
result += doc.content || '';
|
|
2244
|
+
return result;
|
|
2245
|
+
};
|
|
2246
|
+
|
|
2247
|
+
// Component factory (basic implementation)
|
|
2248
|
+
const createComponents = (createElement) => {
|
|
2249
|
+
const components = {};
|
|
2250
|
+
const componentNames = ['Hero', 'Features', 'Pricing', 'CTA', 'Testimonials', 'FAQ', 'Footer', 'Header', 'Nav', 'Card', 'Grid', 'Section', 'Container', 'Button', 'Input', 'Form', 'Modal', 'Table', 'List', 'Badge', 'Alert', 'Progress', 'Spinner', 'Avatar', 'Image', 'Video', 'Code', 'Markdown'];
|
|
2251
|
+
for (const name of componentNames) {
|
|
2252
|
+
components[name] = (props) => createElement(name.toLowerCase() === 'hero' ? 'header' : 'section', { 'data-component': name, ...props });
|
|
2253
|
+
}
|
|
2254
|
+
return components;
|
|
2255
|
+
};
|
|
2256
|
+
|
|
2257
|
+
const getComponentNames = () => ['Hero', 'Features', 'Pricing', 'CTA', 'Testimonials', 'FAQ', 'Footer', 'Header', 'Nav', 'Card', 'Grid', 'Section', 'Container', 'Button', 'Input', 'Form', 'Modal', 'Table', 'List', 'Badge', 'Alert', 'Progress', 'Spinner', 'Avatar', 'Image', 'Video', 'Code', 'Markdown'];
|
|
2258
|
+
|
|
2259
|
+
const getComponentMeta = (name) => ({
|
|
2260
|
+
category: ['Hero', 'Features', 'Pricing', 'CTA', 'Testimonials'].includes(name) ? 'landing' : 'ui',
|
|
2261
|
+
requiredProps: name === 'Hero' ? ['title'] : [],
|
|
2262
|
+
related: name === 'Hero' ? ['CTA', 'Features'] : []
|
|
2263
|
+
});
|
|
2264
|
+
|
|
2265
|
+
const getComponentsByCategory = (category) => {
|
|
2266
|
+
if (category === 'landing') return ['Hero', 'Features', 'Pricing', 'CTA', 'Testimonials'];
|
|
2267
|
+
return ['Card', 'Button', 'Input', 'Form'];
|
|
2268
|
+
};
|
|
2269
|
+
|
|
2270
|
+
const extractTests = (content) => {
|
|
2271
|
+
const tests = [];
|
|
2272
|
+
const regex = /\`\`\`(?:ts|js)\\s+test[^\\n]*\\n([\\s\\S]*?)\`\`\`/g;
|
|
2273
|
+
let match;
|
|
2274
|
+
while ((match = regex.exec(content)) !== null) {
|
|
2275
|
+
tests.push({ code: match[1].trim() });
|
|
2276
|
+
}
|
|
2277
|
+
return tests;
|
|
2278
|
+
};
|
|
2279
|
+
|
|
2280
|
+
const parseMeta = (meta) => {
|
|
2281
|
+
const result = {};
|
|
2282
|
+
const parts = meta.split(/\\s+/);
|
|
2283
|
+
for (const part of parts) {
|
|
2284
|
+
if (part.includes('=')) {
|
|
2285
|
+
const [key, value] = part.split('=');
|
|
2286
|
+
result[key] = value;
|
|
2287
|
+
} else {
|
|
2288
|
+
result[part] = true;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return result;
|
|
2292
|
+
};
|
|
2293
|
+
|
|
2294
|
+
// Simple createElement for testing
|
|
2295
|
+
const createElement = (type, props, ...children) => ({ type, props: props || {}, children });
|
|
2296
|
+
|
|
2297
|
+
// Graph/relationship helper functions
|
|
2298
|
+
const extractLinks = (content) => {
|
|
2299
|
+
const links = [];
|
|
2300
|
+
const regex = /\\[([^\\]]+)\\]\\(([^)]+)\\)/g;
|
|
2301
|
+
let match;
|
|
2302
|
+
while ((match = regex.exec(content)) !== null) {
|
|
2303
|
+
links.push({ text: match[1], url: match[2] });
|
|
2304
|
+
}
|
|
2305
|
+
return links;
|
|
2306
|
+
};
|
|
2307
|
+
|
|
2308
|
+
const extractRelationships = (doc) => {
|
|
2309
|
+
const relationships = [];
|
|
2310
|
+
const url = doc.id || doc.url;
|
|
2311
|
+
if (!url) return relationships;
|
|
2312
|
+
// Extract from content links
|
|
2313
|
+
const links = extractLinks(doc.content || '');
|
|
2314
|
+
for (const link of links) {
|
|
2315
|
+
relationships.push({ from: url, to: link.url, type: 'links', label: link.text });
|
|
2316
|
+
}
|
|
2317
|
+
// Extract from data fields
|
|
2318
|
+
for (const [key, value] of Object.entries(doc.data || {})) {
|
|
2319
|
+
if (typeof value === 'string' && (value.startsWith('http') || value.startsWith('/'))) {
|
|
2320
|
+
relationships.push({ from: url, to: value, type: key });
|
|
2321
|
+
}
|
|
2322
|
+
if (Array.isArray(value)) {
|
|
2323
|
+
for (const item of value) {
|
|
2324
|
+
if (typeof item === 'string' && (item.startsWith('http') || item.startsWith('/'))) {
|
|
2325
|
+
relationships.push({ from: url, to: item, type: key });
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
return relationships;
|
|
2331
|
+
};
|
|
2332
|
+
|
|
2333
|
+
const withRelationships = (doc) => {
|
|
2334
|
+
return { ...doc, relationships: extractRelationships(doc) };
|
|
2335
|
+
};
|
|
2336
|
+
|
|
2337
|
+
const resolveUrl = (entity) => {
|
|
2338
|
+
if (entity.url) return entity.url;
|
|
2339
|
+
if (entity.ns && entity.type && entity.id) {
|
|
2340
|
+
return 'https://' + entity.ns + '/' + entity.type + '/' + entity.id;
|
|
2341
|
+
}
|
|
2342
|
+
if (entity.ns && entity.id) {
|
|
2343
|
+
return 'https://' + entity.ns + '/' + entity.id;
|
|
2344
|
+
}
|
|
2345
|
+
return null;
|
|
2346
|
+
};
|
|
2347
|
+
|
|
2348
|
+
// Context object ($) - unified SDK context (aligned with ai-workflows WorkflowContext)
|
|
2349
|
+
const $ = {
|
|
2350
|
+
ns: __SDK_CONFIG__.ns,
|
|
2351
|
+
db,
|
|
2352
|
+
ai,
|
|
2353
|
+
on,
|
|
2354
|
+
every,
|
|
2355
|
+
send,
|
|
2356
|
+
do: __doEvent__,
|
|
2357
|
+
try: __tryEvent__,
|
|
2358
|
+
queue,
|
|
2359
|
+
actor,
|
|
2360
|
+
actions,
|
|
2361
|
+
artifacts,
|
|
2362
|
+
events,
|
|
2363
|
+
decide,
|
|
2364
|
+
track,
|
|
2365
|
+
experiment,
|
|
2366
|
+
delay,
|
|
2367
|
+
// Workflow state
|
|
2368
|
+
state: {},
|
|
2369
|
+
history: __workflow_history__,
|
|
2370
|
+
// User context
|
|
2371
|
+
user: { id: 'test-user', name: 'Test User', role: 'admin' },
|
|
2372
|
+
request: { method: 'GET', path: '/', headers: {}, body: null },
|
|
2373
|
+
env: { NODE_ENV: 'test' },
|
|
2374
|
+
config: {},
|
|
2375
|
+
context: {},
|
|
2376
|
+
meta: {},
|
|
2377
|
+
// Logging
|
|
2378
|
+
log: (message, data) => {
|
|
2379
|
+
__workflow_history__.push({ type: 'action', name: 'log', data: { message, data }, timestamp: Date.now() });
|
|
2380
|
+
console.log('[sdk] ' + message, data ?? '');
|
|
2381
|
+
},
|
|
2382
|
+
error: console.error,
|
|
2383
|
+
warn: console.warn,
|
|
2384
|
+
// Scoped execution
|
|
2385
|
+
async scope(overrides, fn) {
|
|
2386
|
+
const prev = { ns: $.ns, user: $.user, state: { ...$.state } };
|
|
2387
|
+
Object.assign($, overrides);
|
|
2388
|
+
try { return await fn(); }
|
|
2389
|
+
finally { Object.assign($, prev); }
|
|
2390
|
+
},
|
|
2391
|
+
// Workflow helpers
|
|
2392
|
+
getHandlers() { return { events: Array.from(__event_handlers__.keys()), schedules: __schedule_handlers__.length }; },
|
|
2393
|
+
clearHandlers() { __event_handlers__.clear(); __schedule_handlers__.length = 0; },
|
|
2394
|
+
getHistory() { return [...__workflow_history__]; },
|
|
2395
|
+
clearHistory() { __workflow_history__.length = 0; }
|
|
2396
|
+
};
|
|
2397
|
+
|
|
2398
|
+
// Standalone exports (for import { db, ai, on, send } from 'sdk')
|
|
2399
|
+
const api = {};
|
|
2400
|
+
const search = __db_core__.search.bind(__db_core__);
|
|
2401
|
+
`;
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Generate remote SDK code (RPC-based)
|
|
2405
|
+
*
|
|
2406
|
+
* This creates the platform.do-style SDK with:
|
|
2407
|
+
* - $ - Root context accessor
|
|
2408
|
+
* - db - Database operations
|
|
2409
|
+
* - ai - AI operations
|
|
2410
|
+
* - api - Platform API
|
|
2411
|
+
* - on/send - Event handling
|
|
2412
|
+
*
|
|
2413
|
+
* All operations go through RPC to actual services.
|
|
2414
|
+
*/
|
|
2415
|
+
function generateRemoteSDKCode(config = {}) {
|
|
2416
|
+
const rpcUrl = config.rpcUrl || 'https://rpc.do';
|
|
2417
|
+
const token = config.token || '';
|
|
2418
|
+
const ns = config.ns || 'default';
|
|
2419
|
+
return `
|
|
2420
|
+
// ============================================================
|
|
2421
|
+
// SDK - Thin RPC Proxy ($, db, ai, api, on, send)
|
|
2422
|
+
// ============================================================
|
|
2423
|
+
|
|
2424
|
+
const __SDK_CONFIG__ = {
|
|
2425
|
+
rpcUrl: '${rpcUrl}',
|
|
2426
|
+
token: '${token}',
|
|
2427
|
+
ns: '${ns}'
|
|
2428
|
+
};
|
|
2429
|
+
|
|
2430
|
+
// HTTP RPC client
|
|
2431
|
+
const __rpc__ = {
|
|
2432
|
+
async do(path, ...args) {
|
|
2433
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
2434
|
+
if (__SDK_CONFIG__.token) {
|
|
2435
|
+
headers['Authorization'] = 'Bearer ' + __SDK_CONFIG__.token;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// Serialize functions for remote execution
|
|
2439
|
+
const serializedArgs = args.map(arg => {
|
|
2440
|
+
if (typeof arg === 'function') {
|
|
2441
|
+
return { __fn: arg.toString().replace(/"/g, "'") };
|
|
2442
|
+
}
|
|
2443
|
+
return arg;
|
|
2444
|
+
});
|
|
2445
|
+
|
|
2446
|
+
const response = await fetch(__SDK_CONFIG__.rpcUrl + '/rpc', {
|
|
2447
|
+
method: 'POST',
|
|
2448
|
+
headers,
|
|
2449
|
+
body: JSON.stringify({ method: 'do', path, args: serializedArgs })
|
|
2450
|
+
});
|
|
2451
|
+
|
|
2452
|
+
if (!response.ok) {
|
|
2453
|
+
const errorData = await response.json().catch(() => ({}));
|
|
2454
|
+
throw new Error(errorData.error || 'RPC error: ' + response.statusText);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
return response.json();
|
|
2458
|
+
}
|
|
2459
|
+
};
|
|
2460
|
+
|
|
2461
|
+
// Store for user-defined values
|
|
2462
|
+
const __userDefinitions__ = new Map();
|
|
2463
|
+
|
|
2464
|
+
// Thin proxy that converts property access to RPC paths
|
|
2465
|
+
const __createProxy__ = (path = '') => {
|
|
2466
|
+
// Track stored values for this proxy instance
|
|
2467
|
+
const localStore = new Map();
|
|
2468
|
+
|
|
2469
|
+
const proxy = new Proxy(() => {}, {
|
|
2470
|
+
get: (target, prop, receiver) => {
|
|
2471
|
+
// Handle JSON serialization
|
|
2472
|
+
if (prop === 'toJSON') {
|
|
2473
|
+
// Return stored values as a plain object
|
|
2474
|
+
const obj = { __rpcPath: path };
|
|
2475
|
+
for (const [key, value] of localStore) {
|
|
2476
|
+
obj[key] = value;
|
|
2477
|
+
}
|
|
2478
|
+
for (const [key, value] of __userDefinitions__) {
|
|
2479
|
+
if (key.startsWith(path ? path + '.' : '') && !key.slice(path ? path.length + 1 : 0).includes('.')) {
|
|
2480
|
+
const localKey = key.slice(path ? path.length + 1 : 0);
|
|
2481
|
+
if (localKey) obj[localKey] = value;
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
return () => obj;
|
|
2485
|
+
}
|
|
2486
|
+
if (prop === Symbol.toPrimitive || prop === 'valueOf' || prop === 'toString') {
|
|
2487
|
+
return () => path || '[SDK Proxy]';
|
|
2488
|
+
}
|
|
2489
|
+
if (prop === Symbol.toStringTag) {
|
|
2490
|
+
return 'SDKProxy';
|
|
2491
|
+
}
|
|
2492
|
+
if (prop === 'then' || prop === 'catch' || prop === 'finally') {
|
|
2493
|
+
return undefined; // Don't treat as thenable
|
|
2494
|
+
}
|
|
2495
|
+
// Handle .should by creating an assertion chain
|
|
2496
|
+
if (prop === 'should') {
|
|
2497
|
+
// Build an object from stored values for assertion
|
|
2498
|
+
const obj = {};
|
|
2499
|
+
for (const [key, value] of localStore) {
|
|
2500
|
+
obj[key] = value;
|
|
2501
|
+
}
|
|
2502
|
+
for (const [key, value] of __userDefinitions__) {
|
|
2503
|
+
if (key.startsWith(path ? path + '.' : '') && !key.slice(path ? path.length + 1 : 0).includes('.')) {
|
|
2504
|
+
const localKey = key.slice(path ? path.length + 1 : 0);
|
|
2505
|
+
if (localKey) obj[localKey] = value;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
// If we have stored values, assert on them; otherwise use path string or a marker
|
|
2509
|
+
if (Object.keys(obj).length > 0) {
|
|
2510
|
+
return __createShouldChain__(obj);
|
|
2511
|
+
}
|
|
2512
|
+
// For empty proxy, create a marker object that represents the proxy path
|
|
2513
|
+
return __createShouldChain__(path ? { __path: path } : { __sdk: true, ns: __SDK_CONFIG__.ns });
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
const fullPath = path ? path + '.' + String(prop) : String(prop);
|
|
2517
|
+
|
|
2518
|
+
// Check local store first, then user definitions
|
|
2519
|
+
if (localStore.has(String(prop))) {
|
|
2520
|
+
return localStore.get(String(prop));
|
|
2521
|
+
}
|
|
2522
|
+
if (__userDefinitions__.has(fullPath)) {
|
|
2523
|
+
return __userDefinitions__.get(fullPath);
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
return __createProxy__(fullPath);
|
|
2527
|
+
},
|
|
2528
|
+
|
|
2529
|
+
set: (_, prop, value) => {
|
|
2530
|
+
const fullPath = path ? path + '.' + String(prop) : String(prop);
|
|
2531
|
+
localStore.set(String(prop), value);
|
|
2532
|
+
__userDefinitions__.set(fullPath, value);
|
|
2533
|
+
return true;
|
|
2534
|
+
},
|
|
2535
|
+
|
|
2536
|
+
apply: (_, __, args) => {
|
|
2537
|
+
// Handle tagged template literals
|
|
2538
|
+
if (Array.isArray(args[0]) && 'raw' in args[0]) {
|
|
2539
|
+
const strings = args[0];
|
|
2540
|
+
const values = args.slice(1);
|
|
2541
|
+
const text = strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
|
|
2542
|
+
return __rpc__.do(path, text);
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
return __rpc__.do(path, ...args);
|
|
2546
|
+
},
|
|
2547
|
+
|
|
2548
|
+
// Prevent enumeration from causing infinite loops
|
|
2549
|
+
ownKeys: () => {
|
|
2550
|
+
const keys = [];
|
|
2551
|
+
for (const [key, value] of localStore) {
|
|
2552
|
+
keys.push(key);
|
|
2553
|
+
}
|
|
2554
|
+
for (const [key, value] of __userDefinitions__) {
|
|
2555
|
+
if (key.startsWith(path ? path + '.' : '') && !key.slice(path ? path.length + 1 : 0).includes('.')) {
|
|
2556
|
+
const localKey = key.slice(path ? path.length + 1 : 0);
|
|
2557
|
+
if (localKey && !keys.includes(localKey)) keys.push(localKey);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
return keys;
|
|
2561
|
+
},
|
|
2562
|
+
|
|
2563
|
+
getOwnPropertyDescriptor: (_, prop) => {
|
|
2564
|
+
const fullPath = path ? path + '.' + String(prop) : String(prop);
|
|
2565
|
+
if (localStore.has(String(prop)) || __userDefinitions__.has(fullPath)) {
|
|
2566
|
+
return { configurable: true, enumerable: true, writable: true };
|
|
2567
|
+
}
|
|
2568
|
+
return undefined;
|
|
2569
|
+
}
|
|
2570
|
+
});
|
|
2571
|
+
|
|
2572
|
+
return proxy;
|
|
2573
|
+
};
|
|
2574
|
+
|
|
2575
|
+
// Root proxy and named exports
|
|
2576
|
+
const $ = __createProxy__();
|
|
2577
|
+
const db = $.db;
|
|
2578
|
+
const ai = $.ai;
|
|
2579
|
+
const api = $.api;
|
|
2580
|
+
const on = $.on;
|
|
2581
|
+
const send = $.send;
|
|
2582
|
+
const search = $.search;
|
|
2583
|
+
const track = $.track;
|
|
2584
|
+
const every = $.every;
|
|
2585
|
+
const decide = $.decide;
|
|
2586
|
+
|
|
2587
|
+
// Set default namespace and context properties
|
|
2588
|
+
$.ns = __SDK_CONFIG__.ns;
|
|
2589
|
+
$.user = { id: 'anonymous', name: 'Anonymous', role: 'guest' };
|
|
2590
|
+
$.request = { method: 'GET', path: '/', headers: {}, body: null };
|
|
2591
|
+
$.env = typeof process !== 'undefined' ? (process.env || {}) : {};
|
|
2592
|
+
$.config = {};
|
|
2593
|
+
$.context = {};
|
|
2594
|
+
$.meta = {};
|
|
2595
|
+
`;
|
|
2596
|
+
}
|
|
2597
|
+
/**
|
|
2598
|
+
* Generate .should chainable assertions code
|
|
2599
|
+
*/
|
|
2600
|
+
function generateShouldCode() {
|
|
2601
|
+
return `
|
|
2602
|
+
// ============================================================
|
|
2603
|
+
// Global .should Chainable Assertions
|
|
2604
|
+
// ============================================================
|
|
2605
|
+
|
|
2606
|
+
const __createShouldChain__ = (actual, negated = false) => {
|
|
2607
|
+
const check = (condition, message) => {
|
|
2608
|
+
const passes = negated ? !condition : condition;
|
|
2609
|
+
if (!passes) throw new Error(negated ? 'Expected NOT: ' + message : message);
|
|
2610
|
+
};
|
|
2611
|
+
|
|
2612
|
+
const stringify = (val) => {
|
|
2613
|
+
try {
|
|
2614
|
+
return JSON.stringify(val);
|
|
2615
|
+
} catch {
|
|
2616
|
+
return String(val);
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2619
|
+
|
|
2620
|
+
// Create a lazy chain getter - returns 'this' assertion for chaining
|
|
2621
|
+
const assertion = {};
|
|
2622
|
+
|
|
2623
|
+
// Core assertion methods
|
|
2624
|
+
assertion.equal = (expected) => {
|
|
2625
|
+
check(actual === expected, 'Expected ' + stringify(actual) + ' to equal ' + stringify(expected));
|
|
2626
|
+
return assertion;
|
|
2627
|
+
};
|
|
2628
|
+
assertion.deep = {
|
|
2629
|
+
equal: (expected) => {
|
|
2630
|
+
check(stringify(actual) === stringify(expected), 'Expected deep equal to ' + stringify(expected));
|
|
2631
|
+
return assertion;
|
|
2632
|
+
},
|
|
2633
|
+
include: (expected) => {
|
|
2634
|
+
const actualStr = stringify(actual);
|
|
2635
|
+
const expectedStr = stringify(expected);
|
|
2636
|
+
// Check if expected properties exist with same values
|
|
2637
|
+
const matches = Object.entries(expected || {}).every(([k, v]) =>
|
|
2638
|
+
actual && stringify(actual[k]) === stringify(v)
|
|
2639
|
+
);
|
|
2640
|
+
check(matches, 'Expected ' + actualStr + ' to deeply include ' + expectedStr);
|
|
2641
|
+
return assertion;
|
|
2642
|
+
}
|
|
2643
|
+
};
|
|
2644
|
+
assertion.include = (value) => {
|
|
2645
|
+
if (typeof actual === 'string') check(actual.includes(String(value)), 'Expected "' + actual + '" to include "' + value + '"');
|
|
2646
|
+
else if (Array.isArray(actual)) check(actual.includes(value), 'Expected array to include ' + stringify(value));
|
|
2647
|
+
return assertion;
|
|
2648
|
+
};
|
|
2649
|
+
assertion.contain = assertion.include;
|
|
2650
|
+
assertion.lengthOf = (n) => {
|
|
2651
|
+
check(actual?.length === n, 'Expected length ' + n + ', got ' + actual?.length);
|
|
2652
|
+
return assertion;
|
|
2653
|
+
};
|
|
2654
|
+
assertion.match = (regex) => {
|
|
2655
|
+
const str = String(actual);
|
|
2656
|
+
check(regex.test(str), 'Expected "' + str + '" to match ' + regex);
|
|
2657
|
+
return assertion;
|
|
2658
|
+
};
|
|
2659
|
+
assertion.matches = assertion.match;
|
|
2660
|
+
|
|
2661
|
+
// .be accessor with type checks
|
|
2662
|
+
Object.defineProperty(assertion, 'be', {
|
|
2663
|
+
get: () => {
|
|
2664
|
+
const beObj = {
|
|
2665
|
+
a: (type) => {
|
|
2666
|
+
const actualType = actual === null ? 'null' : Array.isArray(actual) ? 'array' : actual instanceof Date ? 'date' : typeof actual;
|
|
2667
|
+
check(actualType === type.toLowerCase(), 'Expected ' + stringify(actual) + ' to be a ' + type);
|
|
2668
|
+
return assertion;
|
|
2669
|
+
},
|
|
2670
|
+
above: (n) => { check(actual > n, 'Expected ' + actual + ' to be above ' + n); return assertion; },
|
|
2671
|
+
below: (n) => { check(actual < n, 'Expected ' + actual + ' to be below ' + n); return assertion; },
|
|
2672
|
+
within: (min, max) => { check(actual >= min && actual <= max, 'Expected ' + actual + ' to be within ' + min + '..' + max); return assertion; },
|
|
2673
|
+
oneOf: (arr) => { check(Array.isArray(arr) && arr.includes(actual), 'Expected ' + stringify(actual) + ' to be one of ' + stringify(arr)); return assertion; },
|
|
2674
|
+
instanceOf: (cls) => { check(actual instanceof cls, 'Expected to be instance of ' + cls.name); return assertion; }
|
|
2675
|
+
};
|
|
2676
|
+
beObj.an = beObj.a;
|
|
2677
|
+
Object.defineProperty(beObj, 'true', { get: () => { check(actual === true, 'Expected ' + stringify(actual) + ' to be true'); return assertion; } });
|
|
2678
|
+
Object.defineProperty(beObj, 'false', { get: () => { check(actual === false, 'Expected ' + stringify(actual) + ' to be false'); return assertion; } });
|
|
2679
|
+
Object.defineProperty(beObj, 'ok', { get: () => { check(!!actual, 'Expected ' + stringify(actual) + ' to be truthy'); return assertion; } });
|
|
2680
|
+
Object.defineProperty(beObj, 'null', { get: () => { check(actual === null, 'Expected ' + stringify(actual) + ' to be null'); return assertion; } });
|
|
2681
|
+
Object.defineProperty(beObj, 'undefined', { get: () => { check(actual === undefined, 'Expected ' + stringify(actual) + ' to be undefined'); return assertion; } });
|
|
2682
|
+
Object.defineProperty(beObj, 'empty', { get: () => {
|
|
2683
|
+
const isEmpty = actual === '' || (Array.isArray(actual) && actual.length === 0) || (actual && typeof actual === 'object' && Object.keys(actual).length === 0);
|
|
2684
|
+
check(isEmpty, 'Expected ' + stringify(actual) + ' to be empty');
|
|
2685
|
+
return assertion;
|
|
2686
|
+
}});
|
|
2687
|
+
return beObj;
|
|
2688
|
+
}
|
|
2689
|
+
});
|
|
2690
|
+
|
|
2691
|
+
// .have accessor with property/keys/lengthOf/at checks
|
|
2692
|
+
Object.defineProperty(assertion, 'have', {
|
|
2693
|
+
get: () => ({
|
|
2694
|
+
property: (name, value) => {
|
|
2695
|
+
const hasIt = actual != null && Object.prototype.hasOwnProperty.call(actual, name);
|
|
2696
|
+
if (value !== undefined) {
|
|
2697
|
+
check(hasIt && actual[name] === value, "Expected property '" + name + "' = " + stringify(value) + ", got " + stringify(actual?.[name]));
|
|
2698
|
+
} else {
|
|
2699
|
+
check(hasIt, "Expected to have property '" + name + "'");
|
|
2700
|
+
}
|
|
2701
|
+
if (hasIt) return __createShouldChain__(actual[name], negated);
|
|
2702
|
+
return assertion;
|
|
2703
|
+
},
|
|
2704
|
+
keys: (...keys) => {
|
|
2705
|
+
const actualKeys = Object.keys(actual || {});
|
|
2706
|
+
check(keys.every(k => actualKeys.includes(k)), 'Expected to have keys ' + stringify(keys));
|
|
2707
|
+
return assertion;
|
|
2708
|
+
},
|
|
2709
|
+
lengthOf: (n) => {
|
|
2710
|
+
check(actual?.length === n, 'Expected length ' + n + ', got ' + actual?.length);
|
|
2711
|
+
return assertion;
|
|
2712
|
+
},
|
|
2713
|
+
at: {
|
|
2714
|
+
least: (n) => {
|
|
2715
|
+
check(actual?.length >= n, 'Expected length at least ' + n + ', got ' + actual?.length);
|
|
2716
|
+
return assertion;
|
|
2717
|
+
},
|
|
2718
|
+
most: (n) => {
|
|
2719
|
+
check(actual?.length <= n, 'Expected length at most ' + n + ', got ' + actual?.length);
|
|
2720
|
+
return assertion;
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
})
|
|
2724
|
+
});
|
|
2725
|
+
|
|
2726
|
+
// .not negation
|
|
2727
|
+
Object.defineProperty(assertion, 'not', {
|
|
2728
|
+
get: () => __createShouldChain__(actual, !negated)
|
|
2729
|
+
});
|
|
2730
|
+
|
|
2731
|
+
// .with passthrough for readability
|
|
2732
|
+
Object.defineProperty(assertion, 'with', {
|
|
2733
|
+
get: () => assertion
|
|
2734
|
+
});
|
|
2735
|
+
|
|
2736
|
+
// .that passthrough for chaining (e.g. .have.property('x').that.matches(/.../) )
|
|
2737
|
+
Object.defineProperty(assertion, 'that', {
|
|
2738
|
+
get: () => assertion
|
|
2739
|
+
});
|
|
2740
|
+
|
|
2741
|
+
// .and passthrough for chaining
|
|
2742
|
+
Object.defineProperty(assertion, 'and', {
|
|
2743
|
+
get: () => assertion
|
|
2744
|
+
});
|
|
2745
|
+
|
|
2746
|
+
return assertion;
|
|
2747
|
+
};
|
|
2748
|
+
|
|
2749
|
+
// Add .should to Object.prototype
|
|
2750
|
+
Object.defineProperty(Object.prototype, 'should', {
|
|
2751
|
+
get: function() { return __createShouldChain__(this); },
|
|
2752
|
+
configurable: true,
|
|
2753
|
+
enumerable: false
|
|
2754
|
+
});
|
|
2755
|
+
`;
|
|
2756
|
+
}
|
|
2757
|
+
/**
|
|
2758
|
+
* Extract export names from module code
|
|
2759
|
+
* Supports both CommonJS (exports.foo) and ES module (export const foo) syntax
|
|
2760
|
+
*/
|
|
2761
|
+
function getExportNames(moduleCode) {
|
|
2762
|
+
const names = new Set();
|
|
2763
|
+
// Match exports.name = ...
|
|
2764
|
+
const dotPattern = /exports\.(\w+)\s*=/g;
|
|
2765
|
+
let match;
|
|
2766
|
+
while ((match = dotPattern.exec(moduleCode)) !== null) {
|
|
2767
|
+
names.add(match[1]);
|
|
2768
|
+
}
|
|
2769
|
+
// Match exports['name'] = ... or exports["name"] = ...
|
|
2770
|
+
const bracketPattern = /exports\[['"](\w+)['"]\]\s*=/g;
|
|
2771
|
+
while ((match = bracketPattern.exec(moduleCode)) !== null) {
|
|
2772
|
+
names.add(match[1]);
|
|
2773
|
+
}
|
|
2774
|
+
// Match export const name = ... or export let name = ... or export var name = ...
|
|
2775
|
+
const esConstPattern = /export\s+(?:const|let|var)\s+(\w+)\s*=/g;
|
|
2776
|
+
while ((match = esConstPattern.exec(moduleCode)) !== null) {
|
|
2777
|
+
names.add(match[1]);
|
|
2778
|
+
}
|
|
2779
|
+
// Match export function name(...) or export async function name(...)
|
|
2780
|
+
const esFunctionPattern = /export\s+(?:async\s+)?function\s+(\w+)\s*\(/g;
|
|
2781
|
+
while ((match = esFunctionPattern.exec(moduleCode)) !== null) {
|
|
2782
|
+
names.add(match[1]);
|
|
2783
|
+
}
|
|
2784
|
+
// Match export class name
|
|
2785
|
+
const esClassPattern = /export\s+class\s+(\w+)/g;
|
|
2786
|
+
while ((match = esClassPattern.exec(moduleCode)) !== null) {
|
|
2787
|
+
names.add(match[1]);
|
|
2788
|
+
}
|
|
2789
|
+
return Array.from(names).join(', ') || '_unused';
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2792
|
+
* Transform module code to work in sandbox
|
|
2793
|
+
* Converts ES module exports to CommonJS-style for the sandbox
|
|
2794
|
+
*/
|
|
2795
|
+
function transformModuleCode(moduleCode) {
|
|
2796
|
+
let code = moduleCode;
|
|
2797
|
+
// Transform: export const foo = ... → const foo = ...; exports.foo = foo;
|
|
2798
|
+
code = code.replace(/export\s+(const|let|var)\s+(\w+)\s*=/g, '$1 $2 = exports.$2 =');
|
|
2799
|
+
// Transform: export function foo(...) → function foo(...) exports.foo = foo;
|
|
2800
|
+
code = code.replace(/export\s+(async\s+)?function\s+(\w+)/g, '$1function $2');
|
|
2801
|
+
// Add exports for functions after their definition
|
|
2802
|
+
const funcNames = [...moduleCode.matchAll(/export\s+(?:async\s+)?function\s+(\w+)/g)];
|
|
2803
|
+
for (const [, name] of funcNames) {
|
|
2804
|
+
code += `\nexports.${name} = ${name};`;
|
|
2805
|
+
}
|
|
2806
|
+
// Transform: export class Foo → class Foo; exports.Foo = Foo;
|
|
2807
|
+
code = code.replace(/export\s+class\s+(\w+)/g, 'class $1');
|
|
2808
|
+
const classNames = [...moduleCode.matchAll(/export\s+class\s+(\w+)/g)];
|
|
2809
|
+
for (const [, name] of classNames) {
|
|
2810
|
+
code += `\nexports.${name} = ${name};`;
|
|
2811
|
+
}
|
|
2812
|
+
return code;
|
|
2813
|
+
}
|
|
2814
|
+
/**
|
|
2815
|
+
* Wrap script to auto-return the last expression
|
|
2816
|
+
* Converts: `add(1, 2)` → `return add(1, 2)`
|
|
2817
|
+
*/
|
|
2818
|
+
function wrapScriptForReturn(script) {
|
|
2819
|
+
const trimmed = script.trim();
|
|
2820
|
+
if (!trimmed)
|
|
2821
|
+
return script;
|
|
2822
|
+
// If script already contains a return statement anywhere, don't modify
|
|
2823
|
+
if (/\breturn\b/.test(trimmed))
|
|
2824
|
+
return script;
|
|
2825
|
+
// If script starts with throw, don't modify
|
|
2826
|
+
if (/^\s*throw\b/.test(trimmed))
|
|
2827
|
+
return script;
|
|
2828
|
+
// If it's a single expression (no newlines, no semicolons except at end), wrap it
|
|
2829
|
+
const withoutTrailingSemi = trimmed.replace(/;?\s*$/, '');
|
|
2830
|
+
const isSingleLine = !withoutTrailingSemi.includes('\n');
|
|
2831
|
+
// Check if it looks like a single expression (no control flow, no declarations)
|
|
2832
|
+
const startsWithKeyword = /^\s*(const|let|var|if|for|while|switch|try|class|function|async\s+function)\b/.test(withoutTrailingSemi);
|
|
2833
|
+
if (isSingleLine && !startsWithKeyword) {
|
|
2834
|
+
return `return ${withoutTrailingSemi}`;
|
|
2835
|
+
}
|
|
2836
|
+
// For multi-statement scripts, try to return the last expression
|
|
2837
|
+
const lines = trimmed.split('\n');
|
|
2838
|
+
const lastLine = lines[lines.length - 1].trim();
|
|
2839
|
+
// If last line is an expression (not a declaration, control flow, or throw)
|
|
2840
|
+
if (lastLine && !/^\s*(const|let|var|if|for|while|switch|try|class|function|return|throw)\b/.test(lastLine)) {
|
|
2841
|
+
lines[lines.length - 1] = `return ${lastLine.replace(/;?\s*$/, '')}`;
|
|
2842
|
+
return lines.join('\n');
|
|
2843
|
+
}
|
|
2844
|
+
return script;
|
|
2845
|
+
}
|
|
2846
|
+
/**
|
|
2847
|
+
* Generate worker code for production (uses RPC to ai-tests)
|
|
2848
|
+
*/
|
|
2849
|
+
export function generateWorkerCode(options) {
|
|
2850
|
+
const { module: rawModule = '', tests = '', script: rawScript = '', sdk, imports = [] } = options;
|
|
2851
|
+
const sdkConfig = sdk === true ? {} : (sdk || null);
|
|
2852
|
+
const module = rawModule ? transformModuleCode(rawModule) : '';
|
|
2853
|
+
const script = rawScript ? wrapScriptForReturn(rawScript) : '';
|
|
2854
|
+
const exportNames = getExportNames(rawModule);
|
|
2855
|
+
// Hoisted imports (from MDX test files) - placed at true module top level
|
|
2856
|
+
const hoistedImports = imports.length > 0 ? imports.join('\n') + '\n' : '';
|
|
2857
|
+
return `
|
|
2858
|
+
// Sandbox Worker Entry Point
|
|
2859
|
+
import { RpcTarget, newWorkersRpcResponse } from 'capnweb';
|
|
2860
|
+
${hoistedImports}
|
|
2861
|
+
const logs = [];
|
|
2862
|
+
|
|
2863
|
+
${sdkConfig ? generateShouldCode() : ''}
|
|
2864
|
+
|
|
2865
|
+
${sdkConfig ? generateSDKCode(sdkConfig) : '// SDK not enabled'}
|
|
2866
|
+
|
|
2867
|
+
// Capture console output
|
|
2868
|
+
const originalConsole = { ...console };
|
|
2869
|
+
const captureConsole = (level) => (...args) => {
|
|
2870
|
+
logs.push({
|
|
2871
|
+
level,
|
|
2872
|
+
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '),
|
|
2873
|
+
timestamp: Date.now()
|
|
2874
|
+
});
|
|
2875
|
+
originalConsole[level](...args);
|
|
2876
|
+
};
|
|
2877
|
+
console.log = captureConsole('log');
|
|
2878
|
+
console.warn = captureConsole('warn');
|
|
2879
|
+
console.error = captureConsole('error');
|
|
2880
|
+
console.info = captureConsole('info');
|
|
2881
|
+
console.debug = captureConsole('debug');
|
|
2882
|
+
|
|
2883
|
+
// ============================================================
|
|
2884
|
+
// USER MODULE CODE (embedded at generation time)
|
|
2885
|
+
// ============================================================
|
|
2886
|
+
// Module exports object - exports become top-level variables
|
|
2887
|
+
const exports = {};
|
|
2888
|
+
|
|
2889
|
+
${module ? `
|
|
2890
|
+
// Execute module code
|
|
2891
|
+
try {
|
|
2892
|
+
${module}
|
|
2893
|
+
} catch (e) {
|
|
2894
|
+
console.error('Module error:', e.message);
|
|
2895
|
+
}
|
|
2896
|
+
` : '// No module code provided'}
|
|
2897
|
+
|
|
2898
|
+
// Expose all exports as top-level variables for tests and scripts
|
|
2899
|
+
// This allows: export const add = (a, b) => a + b; then later: add(1, 2)
|
|
2900
|
+
${rawModule ? `
|
|
2901
|
+
const { ${exportNames} } = exports;
|
|
2902
|
+
`.trim() : ''}
|
|
2903
|
+
|
|
2904
|
+
// ============================================================
|
|
2905
|
+
// RPC SERVER - Expose exports via capnweb
|
|
2906
|
+
// ============================================================
|
|
2907
|
+
class ExportsRpcTarget extends RpcTarget {
|
|
2908
|
+
// Dynamically expose all exports as RPC methods
|
|
2909
|
+
constructor() {
|
|
2910
|
+
super();
|
|
2911
|
+
for (const [key, value] of Object.entries(exports)) {
|
|
2912
|
+
if (typeof value === 'function') {
|
|
2913
|
+
this[key] = value;
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
// List available exports
|
|
2919
|
+
list() {
|
|
2920
|
+
return Object.keys(exports);
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// Get an export by name
|
|
2924
|
+
get(name) {
|
|
2925
|
+
return exports[name];
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
// ============================================================
|
|
2930
|
+
// WORKER ENTRY POINT
|
|
2931
|
+
// ============================================================
|
|
2932
|
+
export default {
|
|
2933
|
+
async fetch(request, env) {
|
|
2934
|
+
const url = new URL(request.url);
|
|
2935
|
+
|
|
2936
|
+
// Route: GET / - Return info about exports
|
|
2937
|
+
if (request.method === 'GET' && url.pathname === '/') {
|
|
2938
|
+
return Response.json({
|
|
2939
|
+
exports: Object.keys(exports),
|
|
2940
|
+
rpc: '/rpc',
|
|
2941
|
+
execute: '/execute'
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
// Route: /rpc - capnweb RPC to module exports
|
|
2946
|
+
if (url.pathname === '/rpc') {
|
|
2947
|
+
return newWorkersRpcResponse(request, new ExportsRpcTarget());
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
// Route: GET /:name - Simple JSON endpoint to access exports
|
|
2951
|
+
if (request.method === 'GET' && url.pathname !== '/execute') {
|
|
2952
|
+
const name = url.pathname.slice(1); // Remove leading /
|
|
2953
|
+
const value = exports[name];
|
|
2954
|
+
|
|
2955
|
+
// Check if export exists
|
|
2956
|
+
if (!(name in exports)) {
|
|
2957
|
+
return Response.json({ error: \`Export "\${name}" not found\` }, { status: 404 });
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
// If it's not a function, just return the value
|
|
2961
|
+
if (typeof value !== 'function') {
|
|
2962
|
+
return Response.json({ result: value });
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
// It's a function - parse args and call it
|
|
2966
|
+
try {
|
|
2967
|
+
const args = [];
|
|
2968
|
+
const argsParam = url.searchParams.get('args');
|
|
2969
|
+
if (argsParam) {
|
|
2970
|
+
// Support JSON array: ?args=[1,2,3]
|
|
2971
|
+
try {
|
|
2972
|
+
const parsed = JSON.parse(argsParam);
|
|
2973
|
+
if (Array.isArray(parsed)) {
|
|
2974
|
+
args.push(...parsed);
|
|
2975
|
+
} else {
|
|
2976
|
+
args.push(parsed);
|
|
2977
|
+
}
|
|
2978
|
+
} catch {
|
|
2979
|
+
// Not JSON, use as single string arg
|
|
2980
|
+
args.push(argsParam);
|
|
2981
|
+
}
|
|
2982
|
+
} else {
|
|
2983
|
+
// Support named params: ?a=1&b=2 -> passed as object
|
|
2984
|
+
const params = Object.fromEntries(url.searchParams.entries());
|
|
2985
|
+
if (Object.keys(params).length > 0) {
|
|
2986
|
+
// Try to parse numeric values
|
|
2987
|
+
for (const [key, val] of Object.entries(params)) {
|
|
2988
|
+
const num = Number(val);
|
|
2989
|
+
params[key] = !isNaN(num) && val !== '' ? num : val;
|
|
2990
|
+
}
|
|
2991
|
+
args.push(params);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
const result = await value(...args);
|
|
2996
|
+
return Response.json({ result });
|
|
2997
|
+
} catch (e) {
|
|
2998
|
+
return Response.json({ error: e.message }, { status: 500 });
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
// Route: /execute - Run tests and scripts
|
|
3003
|
+
// Check for TEST service binding
|
|
3004
|
+
if (!env.TEST) {
|
|
3005
|
+
return Response.json({
|
|
3006
|
+
success: false,
|
|
3007
|
+
error: 'TEST service binding not available. Ensure ai-tests worker is bound.',
|
|
3008
|
+
logs,
|
|
3009
|
+
duration: 0
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
// Connect to get the TestServiceCore via RPC
|
|
3014
|
+
const testService = await env.TEST.connect();
|
|
3015
|
+
|
|
3016
|
+
// Create global test functions that proxy to the RPC service
|
|
3017
|
+
const describe = (name, fn) => testService.describe(name, fn);
|
|
3018
|
+
const it = (name, fn) => testService.it(name, fn);
|
|
3019
|
+
const test = (name, fn) => testService.test(name, fn);
|
|
3020
|
+
const expect = (value, message) => testService.expect(value, message);
|
|
3021
|
+
const should = (value) => testService.should(value);
|
|
3022
|
+
const assert = testService.assert;
|
|
3023
|
+
const beforeEach = (fn) => testService.beforeEach(fn);
|
|
3024
|
+
const afterEach = (fn) => testService.afterEach(fn);
|
|
3025
|
+
const beforeAll = (fn) => testService.beforeAll(fn);
|
|
3026
|
+
const afterAll = (fn) => testService.afterAll(fn);
|
|
3027
|
+
|
|
3028
|
+
// Add skip/only modifiers
|
|
3029
|
+
it.skip = (name, fn) => testService.skip(name, fn);
|
|
3030
|
+
it.only = (name, fn) => testService.only(name, fn);
|
|
3031
|
+
test.skip = it.skip;
|
|
3032
|
+
test.only = it.only;
|
|
3033
|
+
|
|
3034
|
+
let scriptResult = undefined;
|
|
3035
|
+
let scriptError = null;
|
|
3036
|
+
let testResults = undefined;
|
|
3037
|
+
|
|
3038
|
+
// ============================================================
|
|
3039
|
+
// USER TEST CODE (embedded at generation time)
|
|
3040
|
+
// ============================================================
|
|
3041
|
+
|
|
3042
|
+
${tests ? `
|
|
3043
|
+
// Register tests
|
|
3044
|
+
try {
|
|
3045
|
+
${tests}
|
|
3046
|
+
} catch (e) {
|
|
3047
|
+
console.error('Test registration error:', e.message);
|
|
3048
|
+
}
|
|
3049
|
+
` : '// No test code provided'}
|
|
3050
|
+
|
|
3051
|
+
// Execute user script
|
|
3052
|
+
${script ? `
|
|
3053
|
+
try {
|
|
3054
|
+
scriptResult = await (async () => {
|
|
3055
|
+
${script}
|
|
3056
|
+
})();
|
|
3057
|
+
} catch (e) {
|
|
3058
|
+
console.error('Script error:', e.message);
|
|
3059
|
+
scriptError = e.message;
|
|
3060
|
+
}
|
|
3061
|
+
` : '// No script code provided'}
|
|
3062
|
+
|
|
3063
|
+
// Run tests if any were registered
|
|
3064
|
+
${tests ? `
|
|
3065
|
+
try {
|
|
3066
|
+
testResults = await testService.run();
|
|
3067
|
+
} catch (e) {
|
|
3068
|
+
console.error('Test run error:', e.message);
|
|
3069
|
+
testResults = { total: 0, passed: 0, failed: 1, skipped: 0, tests: [], duration: 0, error: e.message };
|
|
3070
|
+
}
|
|
3071
|
+
` : ''}
|
|
3072
|
+
|
|
3073
|
+
const hasTests = ${tests ? 'true' : 'false'};
|
|
3074
|
+
const success = scriptError === null && (!hasTests || (testResults && testResults.failed === 0));
|
|
3075
|
+
|
|
3076
|
+
return Response.json({
|
|
3077
|
+
success,
|
|
3078
|
+
value: scriptResult,
|
|
3079
|
+
logs,
|
|
3080
|
+
testResults: hasTests ? testResults : undefined,
|
|
3081
|
+
error: scriptError || undefined,
|
|
3082
|
+
duration: 0
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
3085
|
+
};
|
|
3086
|
+
`;
|
|
3087
|
+
}
|
|
3088
|
+
/**
|
|
3089
|
+
* Generate worker code for development (embedded test framework)
|
|
3090
|
+
*
|
|
3091
|
+
* This version bundles the test framework directly into the worker,
|
|
3092
|
+
* avoiding the need for RPC service bindings in local development.
|
|
3093
|
+
*/
|
|
3094
|
+
export function generateDevWorkerCode(options) {
|
|
3095
|
+
const { module: rawModule = '', tests = '', script: rawScript = '', sdk, imports = [] } = options;
|
|
3096
|
+
const sdkConfig = sdk === true ? {} : (sdk || null);
|
|
3097
|
+
const module = rawModule ? transformModuleCode(rawModule) : '';
|
|
3098
|
+
const script = rawScript ? wrapScriptForReturn(rawScript) : '';
|
|
3099
|
+
const exportNames = getExportNames(rawModule);
|
|
3100
|
+
// Hoisted imports (from MDX test files) - placed at true module top level
|
|
3101
|
+
const hoistedImports = imports.length > 0 ? imports.join('\n') + '\n' : '';
|
|
3102
|
+
return `
|
|
3103
|
+
// Sandbox Worker Entry Point (Dev Mode - embedded test framework)
|
|
3104
|
+
${hoistedImports}
|
|
3105
|
+
const logs = [];
|
|
3106
|
+
const testResults = { total: 0, passed: 0, failed: 0, skipped: 0, tests: [], duration: 0 };
|
|
3107
|
+
const pendingTests = [];
|
|
3108
|
+
|
|
3109
|
+
${sdkConfig ? generateShouldCode() : ''}
|
|
3110
|
+
|
|
3111
|
+
${sdkConfig ? generateSDKCode(sdkConfig) : '// SDK not enabled'}
|
|
3112
|
+
|
|
3113
|
+
// Capture console output
|
|
3114
|
+
const originalConsole = { ...console };
|
|
3115
|
+
const captureConsole = (level) => (...args) => {
|
|
3116
|
+
logs.push({
|
|
3117
|
+
level,
|
|
3118
|
+
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '),
|
|
3119
|
+
timestamp: Date.now()
|
|
3120
|
+
});
|
|
3121
|
+
originalConsole[level](...args);
|
|
3122
|
+
};
|
|
3123
|
+
console.log = captureConsole('log');
|
|
3124
|
+
console.warn = captureConsole('warn');
|
|
3125
|
+
console.error = captureConsole('error');
|
|
3126
|
+
console.info = captureConsole('info');
|
|
3127
|
+
console.debug = captureConsole('debug');
|
|
3128
|
+
|
|
3129
|
+
// Test framework (vitest-compatible API)
|
|
3130
|
+
let currentDescribe = '';
|
|
3131
|
+
let beforeEachFns = [];
|
|
3132
|
+
let afterEachFns = [];
|
|
3133
|
+
|
|
3134
|
+
const describe = (name, fn) => {
|
|
3135
|
+
const prev = currentDescribe;
|
|
3136
|
+
const prevBeforeEach = [...beforeEachFns];
|
|
3137
|
+
const prevAfterEach = [...afterEachFns];
|
|
3138
|
+
currentDescribe = currentDescribe ? currentDescribe + ' > ' + name : name;
|
|
3139
|
+
try { fn(); } finally {
|
|
3140
|
+
currentDescribe = prev;
|
|
3141
|
+
beforeEachFns = prevBeforeEach;
|
|
3142
|
+
afterEachFns = prevAfterEach;
|
|
3143
|
+
}
|
|
3144
|
+
};
|
|
3145
|
+
|
|
3146
|
+
// Hooks
|
|
3147
|
+
const beforeEach = (fn) => { beforeEachFns.push(fn); };
|
|
3148
|
+
const afterEach = (fn) => { afterEachFns.push(fn); };
|
|
3149
|
+
|
|
3150
|
+
const it = (name, fn) => {
|
|
3151
|
+
const fullName = currentDescribe ? currentDescribe + ' > ' + name : name;
|
|
3152
|
+
const hooks = { before: [...beforeEachFns], after: [...afterEachFns] };
|
|
3153
|
+
pendingTests.push({ name: fullName, fn, hooks });
|
|
3154
|
+
};
|
|
3155
|
+
const test = it;
|
|
3156
|
+
|
|
3157
|
+
it.skip = (name, fn) => {
|
|
3158
|
+
const fullName = currentDescribe ? currentDescribe + ' > ' + name : name;
|
|
3159
|
+
pendingTests.push({ name: fullName, fn: null, skip: true });
|
|
3160
|
+
};
|
|
3161
|
+
test.skip = it.skip;
|
|
3162
|
+
|
|
3163
|
+
it.only = (name, fn) => {
|
|
3164
|
+
const fullName = currentDescribe ? currentDescribe + ' > ' + name : name;
|
|
3165
|
+
const hooks = { before: [...beforeEachFns], after: [...afterEachFns] };
|
|
3166
|
+
pendingTests.push({ name: fullName, fn, hooks, only: true });
|
|
3167
|
+
};
|
|
3168
|
+
test.only = it.only;
|
|
3169
|
+
|
|
3170
|
+
// Deep equality check
|
|
3171
|
+
const deepEqual = (a, b) => {
|
|
3172
|
+
if (a === b) return true;
|
|
3173
|
+
if (a == null || b == null) return false;
|
|
3174
|
+
if (typeof a !== typeof b) return false;
|
|
3175
|
+
if (typeof a !== 'object') return false;
|
|
3176
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
3177
|
+
if (Array.isArray(a)) {
|
|
3178
|
+
if (a.length !== b.length) return false;
|
|
3179
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
3180
|
+
}
|
|
3181
|
+
const keysA = Object.keys(a);
|
|
3182
|
+
const keysB = Object.keys(b);
|
|
3183
|
+
if (keysA.length !== keysB.length) return false;
|
|
3184
|
+
return keysA.every(k => deepEqual(a[k], b[k]));
|
|
3185
|
+
};
|
|
3186
|
+
|
|
3187
|
+
// Expect implementation with vitest-compatible matchers
|
|
3188
|
+
const expect = (actual) => {
|
|
3189
|
+
const matchers = {
|
|
3190
|
+
toBe: (expected) => {
|
|
3191
|
+
if (actual !== expected) {
|
|
3192
|
+
throw new Error(\`Expected \${JSON.stringify(expected)} but got \${JSON.stringify(actual)}\`);
|
|
3193
|
+
}
|
|
3194
|
+
},
|
|
3195
|
+
toEqual: (expected) => {
|
|
3196
|
+
if (!deepEqual(actual, expected)) {
|
|
3197
|
+
throw new Error(\`Expected \${JSON.stringify(expected)} but got \${JSON.stringify(actual)}\`);
|
|
3198
|
+
}
|
|
3199
|
+
},
|
|
3200
|
+
toStrictEqual: (expected) => {
|
|
3201
|
+
if (!deepEqual(actual, expected)) {
|
|
3202
|
+
throw new Error(\`Expected \${JSON.stringify(expected)} but got \${JSON.stringify(actual)}\`);
|
|
3203
|
+
}
|
|
3204
|
+
},
|
|
3205
|
+
toBeTruthy: () => {
|
|
3206
|
+
if (!actual) throw new Error(\`Expected truthy but got \${JSON.stringify(actual)}\`);
|
|
3207
|
+
},
|
|
3208
|
+
toBeFalsy: () => {
|
|
3209
|
+
if (actual) throw new Error(\`Expected falsy but got \${JSON.stringify(actual)}\`);
|
|
3210
|
+
},
|
|
3211
|
+
toBeNull: () => {
|
|
3212
|
+
if (actual !== null) throw new Error(\`Expected null but got \${JSON.stringify(actual)}\`);
|
|
3213
|
+
},
|
|
3214
|
+
toBeUndefined: () => {
|
|
3215
|
+
if (actual !== undefined) throw new Error(\`Expected undefined but got \${JSON.stringify(actual)}\`);
|
|
3216
|
+
},
|
|
3217
|
+
toBeDefined: () => {
|
|
3218
|
+
if (actual === undefined) throw new Error('Expected defined but got undefined');
|
|
3219
|
+
},
|
|
3220
|
+
toBeNaN: () => {
|
|
3221
|
+
if (!Number.isNaN(actual)) throw new Error(\`Expected NaN but got \${actual}\`);
|
|
3222
|
+
},
|
|
3223
|
+
toContain: (item) => {
|
|
3224
|
+
if (Array.isArray(actual)) {
|
|
3225
|
+
if (!actual.includes(item)) throw new Error(\`Expected array to contain \${JSON.stringify(item)}\`);
|
|
3226
|
+
} else if (typeof actual === 'string') {
|
|
3227
|
+
if (!actual.includes(item)) throw new Error(\`Expected string to contain "\${item}"\`);
|
|
3228
|
+
} else {
|
|
3229
|
+
throw new Error('toContain only works on arrays and strings');
|
|
3230
|
+
}
|
|
3231
|
+
},
|
|
3232
|
+
toContainEqual: (item) => {
|
|
3233
|
+
if (!Array.isArray(actual)) throw new Error('toContainEqual only works on arrays');
|
|
3234
|
+
if (!actual.some(v => deepEqual(v, item))) {
|
|
3235
|
+
throw new Error(\`Expected array to contain \${JSON.stringify(item)}\`);
|
|
3236
|
+
}
|
|
3237
|
+
},
|
|
3238
|
+
toHaveLength: (length) => {
|
|
3239
|
+
if (actual?.length !== length) {
|
|
3240
|
+
throw new Error(\`Expected length \${length} but got \${actual?.length}\`);
|
|
3241
|
+
}
|
|
3242
|
+
},
|
|
3243
|
+
toHaveProperty: function(path, value) {
|
|
3244
|
+
const parts = typeof path === 'string' ? path.split('.') : [path];
|
|
3245
|
+
let obj = actual;
|
|
3246
|
+
for (const part of parts) {
|
|
3247
|
+
if (obj == null || !(part in obj)) {
|
|
3248
|
+
throw new Error(\`Expected object to have property "\${path}"\`);
|
|
3249
|
+
}
|
|
3250
|
+
obj = obj[part];
|
|
3251
|
+
}
|
|
3252
|
+
if (arguments.length > 1 && !deepEqual(obj, value)) {
|
|
3253
|
+
throw new Error(\`Expected property "\${path}" to be \${JSON.stringify(value)} but got \${JSON.stringify(obj)}\`);
|
|
3254
|
+
}
|
|
3255
|
+
},
|
|
3256
|
+
toMatchObject: (expected) => {
|
|
3257
|
+
if (typeof actual !== 'object' || actual === null) {
|
|
3258
|
+
throw new Error('toMatchObject expects an object');
|
|
3259
|
+
}
|
|
3260
|
+
for (const key of Object.keys(expected)) {
|
|
3261
|
+
if (!deepEqual(actual[key], expected[key])) {
|
|
3262
|
+
throw new Error(\`Expected property "\${key}" to be \${JSON.stringify(expected[key])} but got \${JSON.stringify(actual[key])}\`);
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
},
|
|
3266
|
+
toThrow: (expected) => {
|
|
3267
|
+
if (typeof actual !== 'function') throw new Error('toThrow expects a function');
|
|
3268
|
+
let threw = false;
|
|
3269
|
+
let error;
|
|
3270
|
+
try {
|
|
3271
|
+
actual();
|
|
3272
|
+
} catch (e) {
|
|
3273
|
+
threw = true;
|
|
3274
|
+
error = e;
|
|
3275
|
+
}
|
|
3276
|
+
if (!threw) throw new Error('Expected function to throw');
|
|
3277
|
+
if (expected !== undefined) {
|
|
3278
|
+
if (typeof expected === 'string' && !error.message.includes(expected)) {
|
|
3279
|
+
throw new Error(\`Expected error message to contain "\${expected}" but got "\${error.message}"\`);
|
|
3280
|
+
}
|
|
3281
|
+
if (expected instanceof RegExp && !expected.test(error.message)) {
|
|
3282
|
+
throw new Error(\`Expected error message to match \${expected} but got "\${error.message}"\`);
|
|
3283
|
+
}
|
|
3284
|
+
if (typeof expected === 'function' && !(error instanceof expected)) {
|
|
3285
|
+
throw new Error(\`Expected error to be instance of \${expected.name}\`);
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
},
|
|
3289
|
+
toBeGreaterThan: (n) => {
|
|
3290
|
+
if (!(actual > n)) throw new Error(\`Expected \${actual} to be greater than \${n}\`);
|
|
3291
|
+
},
|
|
3292
|
+
toBeLessThan: (n) => {
|
|
3293
|
+
if (!(actual < n)) throw new Error(\`Expected \${actual} to be less than \${n}\`);
|
|
3294
|
+
},
|
|
3295
|
+
toBeGreaterThanOrEqual: (n) => {
|
|
3296
|
+
if (!(actual >= n)) throw new Error(\`Expected \${actual} to be >= \${n}\`);
|
|
3297
|
+
},
|
|
3298
|
+
toBeLessThanOrEqual: (n) => {
|
|
3299
|
+
if (!(actual <= n)) throw new Error(\`Expected \${actual} to be <= \${n}\`);
|
|
3300
|
+
},
|
|
3301
|
+
toBeCloseTo: (n, digits = 2) => {
|
|
3302
|
+
const diff = Math.abs(actual - n);
|
|
3303
|
+
const threshold = Math.pow(10, -digits) / 2;
|
|
3304
|
+
if (diff > threshold) {
|
|
3305
|
+
throw new Error(\`Expected \${actual} to be close to \${n}\`);
|
|
3306
|
+
}
|
|
3307
|
+
},
|
|
3308
|
+
toMatch: (pattern) => {
|
|
3309
|
+
if (typeof actual !== 'string') throw new Error('toMatch expects a string');
|
|
3310
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
3311
|
+
if (!regex.test(actual)) {
|
|
3312
|
+
throw new Error(\`Expected "\${actual}" to match \${pattern}\`);
|
|
3313
|
+
}
|
|
3314
|
+
},
|
|
3315
|
+
toBeInstanceOf: (cls) => {
|
|
3316
|
+
if (!(actual instanceof cls)) {
|
|
3317
|
+
throw new Error(\`Expected instance of \${cls.name}\`);
|
|
3318
|
+
}
|
|
3319
|
+
},
|
|
3320
|
+
toBeTypeOf: (type) => {
|
|
3321
|
+
if (typeof actual !== type) {
|
|
3322
|
+
throw new Error(\`Expected typeof to be "\${type}" but got "\${typeof actual}"\`);
|
|
3323
|
+
}
|
|
3324
|
+
},
|
|
3325
|
+
};
|
|
3326
|
+
|
|
3327
|
+
matchers.not = {
|
|
3328
|
+
toBe: (expected) => {
|
|
3329
|
+
if (actual === expected) throw new Error(\`Expected not \${JSON.stringify(expected)}\`);
|
|
3330
|
+
},
|
|
3331
|
+
toEqual: (expected) => {
|
|
3332
|
+
if (deepEqual(actual, expected)) {
|
|
3333
|
+
throw new Error(\`Expected not equal to \${JSON.stringify(expected)}\`);
|
|
3334
|
+
}
|
|
3335
|
+
},
|
|
3336
|
+
toBeTruthy: () => {
|
|
3337
|
+
if (actual) throw new Error('Expected not truthy');
|
|
3338
|
+
},
|
|
3339
|
+
toBeFalsy: () => {
|
|
3340
|
+
if (!actual) throw new Error('Expected not falsy');
|
|
3341
|
+
},
|
|
3342
|
+
toBeNull: () => {
|
|
3343
|
+
if (actual === null) throw new Error('Expected not null');
|
|
3344
|
+
},
|
|
3345
|
+
toBeUndefined: () => {
|
|
3346
|
+
if (actual === undefined) throw new Error('Expected not undefined');
|
|
3347
|
+
},
|
|
3348
|
+
toBeDefined: () => {
|
|
3349
|
+
if (actual !== undefined) throw new Error('Expected undefined');
|
|
3350
|
+
},
|
|
3351
|
+
toContain: (item) => {
|
|
3352
|
+
if (Array.isArray(actual) && actual.includes(item)) {
|
|
3353
|
+
throw new Error(\`Expected array not to contain \${JSON.stringify(item)}\`);
|
|
3354
|
+
}
|
|
3355
|
+
if (typeof actual === 'string' && actual.includes(item)) {
|
|
3356
|
+
throw new Error(\`Expected string not to contain "\${item}"\`);
|
|
3357
|
+
}
|
|
3358
|
+
},
|
|
3359
|
+
toHaveProperty: (path) => {
|
|
3360
|
+
const parts = typeof path === 'string' ? path.split('.') : [path];
|
|
3361
|
+
let obj = actual;
|
|
3362
|
+
try {
|
|
3363
|
+
for (const part of parts) {
|
|
3364
|
+
if (obj == null || !(part in obj)) return;
|
|
3365
|
+
obj = obj[part];
|
|
3366
|
+
}
|
|
3367
|
+
throw new Error(\`Expected object not to have property "\${path}"\`);
|
|
3368
|
+
} catch {}
|
|
3369
|
+
},
|
|
3370
|
+
toThrow: () => {
|
|
3371
|
+
if (typeof actual !== 'function') throw new Error('toThrow expects a function');
|
|
3372
|
+
try {
|
|
3373
|
+
actual();
|
|
3374
|
+
} catch (e) {
|
|
3375
|
+
throw new Error('Expected function not to throw');
|
|
3376
|
+
}
|
|
3377
|
+
},
|
|
3378
|
+
toMatch: (pattern) => {
|
|
3379
|
+
if (typeof actual !== 'string') throw new Error('toMatch expects a string');
|
|
3380
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
3381
|
+
if (regex.test(actual)) {
|
|
3382
|
+
throw new Error(\`Expected "\${actual}" not to match \${pattern}\`);
|
|
3383
|
+
}
|
|
3384
|
+
},
|
|
3385
|
+
};
|
|
3386
|
+
|
|
3387
|
+
matchers.resolves = new Proxy({}, {
|
|
3388
|
+
get: (_, prop) => async (...args) => {
|
|
3389
|
+
const resolved = await actual;
|
|
3390
|
+
return expect(resolved)[prop](...args);
|
|
3391
|
+
}
|
|
3392
|
+
});
|
|
3393
|
+
|
|
3394
|
+
matchers.rejects = new Proxy({}, {
|
|
3395
|
+
get: (_, prop) => async (...args) => {
|
|
3396
|
+
try {
|
|
3397
|
+
await actual;
|
|
3398
|
+
throw new Error('Expected promise to reject');
|
|
3399
|
+
} catch (e) {
|
|
3400
|
+
if (e.message === 'Expected promise to reject') throw e;
|
|
3401
|
+
return expect(e)[prop](...args);
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
});
|
|
3405
|
+
|
|
3406
|
+
return matchers;
|
|
3407
|
+
};
|
|
3408
|
+
|
|
3409
|
+
// ============================================================
|
|
3410
|
+
// USER MODULE CODE (embedded at generation time)
|
|
3411
|
+
// ============================================================
|
|
3412
|
+
// Module exports object - exports become top-level variables
|
|
3413
|
+
const exports = {};
|
|
3414
|
+
|
|
3415
|
+
${module ? `
|
|
3416
|
+
// Execute module code
|
|
3417
|
+
try {
|
|
3418
|
+
${module}
|
|
3419
|
+
} catch (e) {
|
|
3420
|
+
console.error('Module error:', e.message);
|
|
3421
|
+
}
|
|
3422
|
+
` : '// No module code provided'}
|
|
3423
|
+
|
|
3424
|
+
// Expose all exports as top-level variables for tests and scripts
|
|
3425
|
+
// This allows: export const add = (a, b) => a + b; then later: add(1, 2)
|
|
3426
|
+
${rawModule ? `
|
|
3427
|
+
const { ${exportNames} } = exports;
|
|
3428
|
+
`.trim() : ''}
|
|
3429
|
+
|
|
3430
|
+
// ============================================================
|
|
3431
|
+
// USER TEST CODE (embedded at generation time)
|
|
3432
|
+
// ============================================================
|
|
3433
|
+
${tests ? `
|
|
3434
|
+
// Register tests
|
|
3435
|
+
try {
|
|
3436
|
+
${tests}
|
|
3437
|
+
} catch (e) {
|
|
3438
|
+
console.error('Test registration error:', e.message);
|
|
3439
|
+
}
|
|
3440
|
+
` : '// No test code provided'}
|
|
3441
|
+
|
|
3442
|
+
// ============================================================
|
|
3443
|
+
// SIMPLE RPC HANDLER (dev mode - no capnweb dependency)
|
|
3444
|
+
// ============================================================
|
|
3445
|
+
async function handleRpc(request) {
|
|
3446
|
+
try {
|
|
3447
|
+
const { method, args = [] } = await request.json();
|
|
3448
|
+
if (method === 'list') {
|
|
3449
|
+
return Response.json({ result: Object.keys(exports) });
|
|
3450
|
+
}
|
|
3451
|
+
if (method === 'get') {
|
|
3452
|
+
const [name] = args;
|
|
3453
|
+
const value = exports[name];
|
|
3454
|
+
if (typeof value === 'function') {
|
|
3455
|
+
return Response.json({ result: { type: 'function', name } });
|
|
3456
|
+
}
|
|
3457
|
+
return Response.json({ result: value });
|
|
3458
|
+
}
|
|
3459
|
+
// Call an exported function
|
|
3460
|
+
const fn = exports[method];
|
|
3461
|
+
if (typeof fn !== 'function') {
|
|
3462
|
+
return Response.json({ error: \`Export "\${method}" is not a function\` }, { status: 400 });
|
|
3463
|
+
}
|
|
3464
|
+
const result = await fn(...args);
|
|
3465
|
+
return Response.json({ result });
|
|
3466
|
+
} catch (e) {
|
|
3467
|
+
return Response.json({ error: e.message }, { status: 500 });
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
// ============================================================
|
|
3472
|
+
// WORKER ENTRY POINT
|
|
3473
|
+
// ============================================================
|
|
3474
|
+
export default {
|
|
3475
|
+
async fetch(request, env) {
|
|
3476
|
+
const url = new URL(request.url);
|
|
3477
|
+
|
|
3478
|
+
// Route: GET / - Return info about exports
|
|
3479
|
+
if (request.method === 'GET' && url.pathname === '/') {
|
|
3480
|
+
return Response.json({
|
|
3481
|
+
exports: Object.keys(exports),
|
|
3482
|
+
rpc: '/rpc',
|
|
3483
|
+
execute: '/execute'
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
// Route: POST /rpc - Simple RPC to module exports
|
|
3488
|
+
if (url.pathname === '/rpc' && request.method === 'POST') {
|
|
3489
|
+
return handleRpc(request);
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
// Route: GET /:name - Simple JSON endpoint to access exports
|
|
3493
|
+
if (request.method === 'GET' && url.pathname !== '/execute') {
|
|
3494
|
+
const name = url.pathname.slice(1);
|
|
3495
|
+
const value = exports[name];
|
|
3496
|
+
|
|
3497
|
+
// Check if export exists
|
|
3498
|
+
if (!(name in exports)) {
|
|
3499
|
+
return Response.json({ error: \`Export "\${name}" not found\` }, { status: 404 });
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
// If it's not a function, just return the value
|
|
3503
|
+
if (typeof value !== 'function') {
|
|
3504
|
+
return Response.json({ result: value });
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
// It's a function - parse args and call it
|
|
3508
|
+
try {
|
|
3509
|
+
const args = [];
|
|
3510
|
+
const argsParam = url.searchParams.get('args');
|
|
3511
|
+
if (argsParam) {
|
|
3512
|
+
try {
|
|
3513
|
+
const parsed = JSON.parse(argsParam);
|
|
3514
|
+
if (Array.isArray(parsed)) args.push(...parsed);
|
|
3515
|
+
else args.push(parsed);
|
|
3516
|
+
} catch {
|
|
3517
|
+
args.push(argsParam);
|
|
3518
|
+
}
|
|
3519
|
+
} else {
|
|
3520
|
+
const params = Object.fromEntries(url.searchParams.entries());
|
|
3521
|
+
if (Object.keys(params).length > 0) {
|
|
3522
|
+
for (const [key, val] of Object.entries(params)) {
|
|
3523
|
+
const num = Number(val);
|
|
3524
|
+
params[key] = !isNaN(num) && val !== '' ? num : val;
|
|
3525
|
+
}
|
|
3526
|
+
args.push(params);
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
const result = await value(...args);
|
|
3530
|
+
return Response.json({ result });
|
|
3531
|
+
} catch (e) {
|
|
3532
|
+
return Response.json({ error: e.message }, { status: 500 });
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
// Route: /execute - Run tests and scripts
|
|
3537
|
+
let scriptResult = undefined;
|
|
3538
|
+
let scriptError = null;
|
|
3539
|
+
|
|
3540
|
+
// Execute user script
|
|
3541
|
+
${script ? `
|
|
3542
|
+
try {
|
|
3543
|
+
scriptResult = (() => {
|
|
3544
|
+
${script}
|
|
3545
|
+
})();
|
|
3546
|
+
// Support async scripts
|
|
3547
|
+
if (scriptResult && typeof scriptResult.then === 'function') {
|
|
3548
|
+
scriptResult = await scriptResult;
|
|
3549
|
+
}
|
|
3550
|
+
} catch (e) {
|
|
3551
|
+
console.error('Script error:', e.message);
|
|
3552
|
+
scriptError = e.message;
|
|
3553
|
+
}
|
|
3554
|
+
` : '// No script code provided'}
|
|
3555
|
+
|
|
3556
|
+
// Run all pending tests
|
|
3557
|
+
const testStart = Date.now();
|
|
3558
|
+
const hasOnly = pendingTests.some(t => t.only);
|
|
3559
|
+
const testsToRun = hasOnly ? pendingTests.filter(t => t.only || t.skip) : pendingTests;
|
|
3560
|
+
|
|
3561
|
+
for (const { name, fn, hooks, skip } of testsToRun) {
|
|
3562
|
+
testResults.total++;
|
|
3563
|
+
|
|
3564
|
+
if (skip) {
|
|
3565
|
+
testResults.skipped++;
|
|
3566
|
+
testResults.tests.push({ name, passed: true, skipped: true, duration: 0 });
|
|
3567
|
+
continue;
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
const start = Date.now();
|
|
3571
|
+
try {
|
|
3572
|
+
// Run beforeEach hooks
|
|
3573
|
+
if (hooks?.before) {
|
|
3574
|
+
for (const hook of hooks.before) {
|
|
3575
|
+
const hookResult = hook();
|
|
3576
|
+
if (hookResult && typeof hookResult.then === 'function') {
|
|
3577
|
+
await hookResult;
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
// Run the test
|
|
3583
|
+
const result = fn();
|
|
3584
|
+
if (result && typeof result.then === 'function') {
|
|
3585
|
+
await result;
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
// Run afterEach hooks
|
|
3589
|
+
if (hooks?.after) {
|
|
3590
|
+
for (const hook of hooks.after) {
|
|
3591
|
+
const hookResult = hook();
|
|
3592
|
+
if (hookResult && typeof hookResult.then === 'function') {
|
|
3593
|
+
await hookResult;
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
testResults.passed++;
|
|
3599
|
+
testResults.tests.push({ name, passed: true, duration: Date.now() - start });
|
|
3600
|
+
} catch (e) {
|
|
3601
|
+
testResults.failed++;
|
|
3602
|
+
testResults.tests.push({
|
|
3603
|
+
name,
|
|
3604
|
+
passed: false,
|
|
3605
|
+
error: e.message || String(e),
|
|
3606
|
+
duration: Date.now() - start
|
|
3607
|
+
});
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
testResults.duration = Date.now() - testStart;
|
|
3612
|
+
|
|
3613
|
+
const hasTests = ${tests ? 'true' : 'false'};
|
|
3614
|
+
const success = scriptError === null && (!hasTests || testResults.failed === 0);
|
|
3615
|
+
|
|
3616
|
+
return Response.json({
|
|
3617
|
+
success,
|
|
3618
|
+
value: scriptResult,
|
|
3619
|
+
logs,
|
|
3620
|
+
testResults: hasTests ? testResults : undefined,
|
|
3621
|
+
error: scriptError || undefined,
|
|
3622
|
+
duration: 0
|
|
3623
|
+
});
|
|
3624
|
+
}
|
|
3625
|
+
};
|
|
3626
|
+
`;
|
|
3627
|
+
}
|
|
3628
|
+
//# sourceMappingURL=worker-template.js.map
|