@ai-matrx/applets 0.2.7
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/CHANGELOG.md +130 -0
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/context-Dz3PWsJd.d.cts +193 -0
- package/dist/context-Dz3PWsJd.d.ts +193 -0
- package/dist/frame.cjs +977 -0
- package/dist/frame.cjs.map +1 -0
- package/dist/frame.d.cts +42 -0
- package/dist/frame.d.ts +42 -0
- package/dist/frame.js +958 -0
- package/dist/frame.js.map +1 -0
- package/dist/host.cjs +664 -0
- package/dist/host.cjs.map +1 -0
- package/dist/host.d.cts +128 -0
- package/dist/host.d.ts +128 -0
- package/dist/host.js +641 -0
- package/dist/host.js.map +1 -0
- package/dist/index.cjs +66 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +200 -0
- package/dist/index.d.ts +200 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.cjs +699 -0
- package/dist/platform.cjs.map +1 -0
- package/dist/platform.d.cts +135 -0
- package/dist/platform.d.ts +135 -0
- package/dist/platform.js +680 -0
- package/dist/platform.js.map +1 -0
- package/dist/react.cjs +880 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +178 -0
- package/dist/react.d.ts +178 -0
- package/dist/react.js +854 -0
- package/dist/react.js.map +1 -0
- package/package.json +128 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/platform/index.ts
|
|
21
|
+
var platform_exports = {};
|
|
22
|
+
__export(platform_exports, {
|
|
23
|
+
APPLET_BASE_SURFACE: () => APPLET_BASE_SURFACE,
|
|
24
|
+
DEFINITION_COLUMNS: () => DEFINITION_COLUMNS,
|
|
25
|
+
appletSurfaceName: () => appletSurfaceName,
|
|
26
|
+
createPlatformHost: () => createPlatformHost,
|
|
27
|
+
definitionToRecord: () => definitionToRecord
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(platform_exports);
|
|
30
|
+
|
|
31
|
+
// src/platform/host.ts
|
|
32
|
+
var import_core = require("@ai-matrx/records/core");
|
|
33
|
+
var import_realtime = require("@ai-matrx/records/realtime");
|
|
34
|
+
var import_data = require("@ai-matrx/records/data");
|
|
35
|
+
var import_actions = require("@ai-matrx/alchemy/actions");
|
|
36
|
+
|
|
37
|
+
// src/types.ts
|
|
38
|
+
var PAGE_ACTION_STREAM = "actions";
|
|
39
|
+
var NAV_STREAM = "nav";
|
|
40
|
+
var valueStream = (name) => `value:${name}`;
|
|
41
|
+
function ok(data) {
|
|
42
|
+
return { ok: true, data };
|
|
43
|
+
}
|
|
44
|
+
function fail(code, message, retryable = false, diagnostic) {
|
|
45
|
+
const error = { code, message, retryable };
|
|
46
|
+
if (diagnostic !== void 0) error.diagnostic = diagnostic;
|
|
47
|
+
return { ok: false, error };
|
|
48
|
+
}
|
|
49
|
+
function fromRecordsError(error) {
|
|
50
|
+
const out = { code: error.code, message: error.message, retryable: error.code === "unreachable" || error.code === "timed_out" };
|
|
51
|
+
if (error.diagnostic !== void 0) out.diagnostic = error.diagnostic;
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
function toAppletError(err, code = "internal") {
|
|
55
|
+
if (err && typeof err === "object") {
|
|
56
|
+
const o = err;
|
|
57
|
+
if (typeof o.code === "string" && typeof o.message === "string") {
|
|
58
|
+
const out = { code: o.code, message: o.message, retryable: o.retryable === true };
|
|
59
|
+
if (typeof o.diagnostic === "string") out.diagnostic = o.diagnostic;
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
if (err instanceof Error) return { code, message: err.message, retryable: false };
|
|
63
|
+
}
|
|
64
|
+
return { code, message: String(err), retryable: false };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/platform/supabase.ts
|
|
68
|
+
function asPlatformSupabase(client) {
|
|
69
|
+
const c = client;
|
|
70
|
+
if (typeof c.schema !== "function" || typeof c.auth?.getSession !== "function") {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"[applets/platform] createPlatformHost needs a supabase-js v2 client carrying the viewer's session (it has no schema() or auth.getSession())."
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return c;
|
|
76
|
+
}
|
|
77
|
+
function readError(error, code = "read_failed") {
|
|
78
|
+
const out = { code: error.code ? `${code}:${error.code}` : code, message: error.message, retryable: false };
|
|
79
|
+
if (error.details) out.diagnostic = error.details;
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
async function read(chain, shape) {
|
|
83
|
+
try {
|
|
84
|
+
const answer = await chain;
|
|
85
|
+
if (answer.error) return { ok: false, error: readError(answer.error) };
|
|
86
|
+
return { ok: true, data: shape(answer.data) };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
error: { code: "unreachable", message: err instanceof Error ? err.message : String(err), retryable: true }
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function isRecord(value) {
|
|
95
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/platform/record.ts
|
|
99
|
+
var DEFINITION_COLUMNS = "id, version, organization_id, slug, name, status, files, entry, pages, mandates, sources, parent_applet_id, allowed_imports, component_code";
|
|
100
|
+
var APPLET_BASE_SURFACE = "applets/applet";
|
|
101
|
+
var appletSurfaceName = (appletId) => `applets/${appletId}`;
|
|
102
|
+
function strings(value) {
|
|
103
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
104
|
+
}
|
|
105
|
+
function files(value) {
|
|
106
|
+
const out = {};
|
|
107
|
+
if (!isRecord(value)) return out;
|
|
108
|
+
for (const [name, source] of Object.entries(value)) if (typeof source === "string") out[name] = source;
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
function pages(value) {
|
|
112
|
+
if (!Array.isArray(value)) return [];
|
|
113
|
+
const out = [];
|
|
114
|
+
for (const p of value) {
|
|
115
|
+
if (!isRecord(p) || typeof p.path !== "string" || typeof p.file !== "string") continue;
|
|
116
|
+
const page = { path: p.path, title: typeof p.title === "string" ? p.title : p.path, file: p.file };
|
|
117
|
+
if (typeof p.parent === "string") page.parent = p.parent;
|
|
118
|
+
out.push(page);
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
function mandates(value) {
|
|
123
|
+
if (!Array.isArray(value)) return [];
|
|
124
|
+
return value.flatMap((m) => isRecord(m) && typeof m.alias === "string" && typeof m.key === "string" ? [{ alias: m.alias, key: m.key }] : []);
|
|
125
|
+
}
|
|
126
|
+
function sources(value) {
|
|
127
|
+
if (!Array.isArray(value)) return [];
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const s of value) {
|
|
130
|
+
if (!isRecord(s) || typeof s.alias !== "string") continue;
|
|
131
|
+
if (typeof s.table_id === "string" && typeof s.organization_id === "string") {
|
|
132
|
+
out.push({ alias: s.alias, table_id: s.table_id, organization_id: s.organization_id });
|
|
133
|
+
} else if (typeof s.entity === "string") {
|
|
134
|
+
out.push({ alias: s.alias, entity: s.entity });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
function definitionToRecord(row) {
|
|
140
|
+
if (!isRecord(row) || typeof row.id !== "string") return { ok: false, message: "The Applet row has no id." };
|
|
141
|
+
if (typeof row.organization_id !== "string") return { ok: false, message: `Applet ${row.id} has no organization.` };
|
|
142
|
+
const id = row.id;
|
|
143
|
+
let recordFiles = files(row.files);
|
|
144
|
+
let entry = typeof row.entry === "string" && row.entry ? row.entry : "";
|
|
145
|
+
let recordPages = pages(row.pages);
|
|
146
|
+
if ((Object.keys(recordFiles).length === 0 || !entry) && typeof row.component_code === "string" && row.component_code) {
|
|
147
|
+
recordFiles = { "App.tsx": row.component_code };
|
|
148
|
+
entry = "App.tsx";
|
|
149
|
+
if (recordPages.length === 0) {
|
|
150
|
+
recordPages = [{ path: "/", title: typeof row.name === "string" && row.name ? row.name : "Home", file: "App.tsx" }];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!entry || recordFiles[entry] === void 0) {
|
|
154
|
+
return { ok: false, message: `Applet ${id} has no entry file to run.` };
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
record: {
|
|
159
|
+
id,
|
|
160
|
+
version: typeof row.version === "number" ? row.version : 1,
|
|
161
|
+
organizationId: row.organization_id,
|
|
162
|
+
slug: typeof row.slug === "string" ? row.slug : id,
|
|
163
|
+
published: row.status === "published",
|
|
164
|
+
files: recordFiles,
|
|
165
|
+
entry,
|
|
166
|
+
scope: { entries: strings(row.allowed_imports), shadowDangerousGlobals: true },
|
|
167
|
+
pages: recordPages,
|
|
168
|
+
mandates: mandates(row.mandates),
|
|
169
|
+
sources: sources(row.sources),
|
|
170
|
+
parentId: typeof row.parent_applet_id === "string" ? row.parent_applet_id : null,
|
|
171
|
+
surfaceName: appletSurfaceName(id)
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/platform/host.ts
|
|
177
|
+
var BASE_PAGE_ACTIONS = /* @__PURE__ */ new Set(["write_row"]);
|
|
178
|
+
var MAX_CHAIN = 16;
|
|
179
|
+
function mint(world, prefix) {
|
|
180
|
+
world.seq += 1;
|
|
181
|
+
const c = globalThis.crypto;
|
|
182
|
+
return `${prefix}_${c?.randomUUID ? c.randomUUID() : `${Date.now().toString(36)}${world.seq}`}`;
|
|
183
|
+
}
|
|
184
|
+
function clone(value) {
|
|
185
|
+
return value === void 0 ? value : JSON.parse(JSON.stringify(value));
|
|
186
|
+
}
|
|
187
|
+
function refuseRecords(code, message) {
|
|
188
|
+
return { ok: false, error: { code, message } };
|
|
189
|
+
}
|
|
190
|
+
function createPlatformHost(opts) {
|
|
191
|
+
const world = {
|
|
192
|
+
opts,
|
|
193
|
+
supabase: asPlatformSupabase(opts.supabase),
|
|
194
|
+
hosts: /* @__PURE__ */ new Map(),
|
|
195
|
+
pending: /* @__PURE__ */ new Map(),
|
|
196
|
+
viewer: null,
|
|
197
|
+
records: null,
|
|
198
|
+
seq: 0,
|
|
199
|
+
stopNav: null
|
|
200
|
+
};
|
|
201
|
+
const root = buildHost(world, opts.appletId, `platform:${opts.appletId}`, null);
|
|
202
|
+
if (opts.nav.subscribe) {
|
|
203
|
+
world.stopNav = opts.nav.subscribe((location) => {
|
|
204
|
+
for (const h of world.hosts.values()) h.emit(NAV_STREAM, location);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return root;
|
|
208
|
+
}
|
|
209
|
+
function loadViewer(world) {
|
|
210
|
+
world.viewer ??= (async () => {
|
|
211
|
+
const report = (error, where) => void world.opts.reportError({ ...error, where });
|
|
212
|
+
let userId = null;
|
|
213
|
+
try {
|
|
214
|
+
const session = await world.supabase.auth.getSession();
|
|
215
|
+
if (session.error) report({ code: "session_unreadable", message: session.error.message, retryable: true }, "viewer.session");
|
|
216
|
+
userId = session.data.session?.user.id ?? null;
|
|
217
|
+
} catch (err) {
|
|
218
|
+
report(toAppletError(err, "session_unreadable"), "viewer.session");
|
|
219
|
+
}
|
|
220
|
+
if (!userId) return { userId: null, organizationIds: [], activeOrganizationId: null, guest: true };
|
|
221
|
+
const memberships = await read(
|
|
222
|
+
world.supabase.schema("iam").from("organization_member").select("organization_id").eq("user_id", userId),
|
|
223
|
+
(data) => [...new Set((Array.isArray(data) ? data : []).flatMap((r) => isRecord(r) && typeof r.organization_id === "string" ? [r.organization_id] : []))]
|
|
224
|
+
);
|
|
225
|
+
if (!memberships.ok) report(memberships.error, "viewer.memberships");
|
|
226
|
+
return {
|
|
227
|
+
userId,
|
|
228
|
+
organizationIds: memberships.ok ? memberships.data : [],
|
|
229
|
+
activeOrganizationId: world.opts.activeOrganizationId,
|
|
230
|
+
guest: false
|
|
231
|
+
};
|
|
232
|
+
})().catch((err) => {
|
|
233
|
+
world.viewer = null;
|
|
234
|
+
throw err;
|
|
235
|
+
});
|
|
236
|
+
return world.viewer;
|
|
237
|
+
}
|
|
238
|
+
function loadRecordsClient(world) {
|
|
239
|
+
world.records ??= loadViewer(world).then((viewer) => {
|
|
240
|
+
const actor = viewer.userId ? { actor: "user", user_id: viewer.userId } : { actor: "user" };
|
|
241
|
+
return (0, import_core.createRecordsClient)({
|
|
242
|
+
dataSource: world.opts.supabase,
|
|
243
|
+
actor,
|
|
244
|
+
// Writes to a custom table go to the TABLE's organization (createTableDataPort's `clientIn`);
|
|
245
|
+
// reads span every membership. Nothing here narrows a read to the active organization.
|
|
246
|
+
organizationId: world.opts.activeOrganizationId,
|
|
247
|
+
organizationIds: viewer.organizationIds,
|
|
248
|
+
onError: (e) => void world.opts.reportError({ code: e.code, message: e.message, retryable: false, where: `records:${e.where}` }),
|
|
249
|
+
realtime: world.opts.records?.realtime ?? (0, import_realtime.createRecordsRealtimePort)(),
|
|
250
|
+
...world.opts.records?.knownDoors ? { knownDoors: world.opts.records.knownDoors } : {}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
return world.records;
|
|
254
|
+
}
|
|
255
|
+
function buildHost(world, appletId, hostId, parent) {
|
|
256
|
+
const { opts, supabase } = world;
|
|
257
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
258
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
259
|
+
const values = /* @__PURE__ */ new Map();
|
|
260
|
+
let recordPromise = null;
|
|
261
|
+
let mounted = null;
|
|
262
|
+
let childCount = 0;
|
|
263
|
+
const report = (error, where) => {
|
|
264
|
+
void Promise.resolve(opts.reportError({ ...error, where })).catch(() => void 0);
|
|
265
|
+
};
|
|
266
|
+
const deliver = (subscriptionId, payload) => {
|
|
267
|
+
const message = { subscriptionId, payload: clone(payload) };
|
|
268
|
+
for (const l of [...listeners]) l(message);
|
|
269
|
+
};
|
|
270
|
+
const emit = (stream, payload) => {
|
|
271
|
+
for (const [id, s] of subscriptions) if (s.stream === stream) deliver(id, payload);
|
|
272
|
+
};
|
|
273
|
+
const record = () => {
|
|
274
|
+
recordPromise ??= (async () => {
|
|
275
|
+
const answer = await read(
|
|
276
|
+
supabase.schema("app").from("definition").select(DEFINITION_COLUMNS).eq("id", appletId).is("deleted_at", null).maybeSingle(),
|
|
277
|
+
(data2) => data2
|
|
278
|
+
);
|
|
279
|
+
if (!answer.ok) throw answer.error;
|
|
280
|
+
if (answer.data === null) {
|
|
281
|
+
throw { code: "applet_not_found", message: `There is no Applet ${appletId} you can open.`, retryable: false };
|
|
282
|
+
}
|
|
283
|
+
const mapped = definitionToRecord(answer.data);
|
|
284
|
+
if (!mapped.ok) throw { code: "applet_unreadable", message: mapped.message, retryable: false };
|
|
285
|
+
return mapped.record;
|
|
286
|
+
})().catch((err) => {
|
|
287
|
+
recordPromise = null;
|
|
288
|
+
throw err;
|
|
289
|
+
});
|
|
290
|
+
return recordPromise.then(clone);
|
|
291
|
+
};
|
|
292
|
+
let dataPort = null;
|
|
293
|
+
const loadData = () => {
|
|
294
|
+
dataPort ??= (async () => {
|
|
295
|
+
const rec = await record();
|
|
296
|
+
const aliases = {};
|
|
297
|
+
const entities = /* @__PURE__ */ new Map();
|
|
298
|
+
for (const s of rec.sources) {
|
|
299
|
+
if ("table_id" in s) aliases[s.alias] = { kind: "table", tableId: s.table_id, organizationId: s.organization_id };
|
|
300
|
+
else entities.set(s.alias, s.entity);
|
|
301
|
+
}
|
|
302
|
+
const port = Object.keys(aliases).length ? (0, import_data.createTableDataPort)({ client: await loadRecordsClient(world), aliases }) : null;
|
|
303
|
+
return { port, entities };
|
|
304
|
+
})().catch((err) => {
|
|
305
|
+
dataPort = null;
|
|
306
|
+
throw err;
|
|
307
|
+
});
|
|
308
|
+
return dataPort;
|
|
309
|
+
};
|
|
310
|
+
const entityRefusal = (alias, entity) => refuseRecords("not_supported", `"${alias}" is a ${entity} source; platform record sources are not readable from an Applet yet (AP-3 phase B).`);
|
|
311
|
+
const routed = async (alias, call) => {
|
|
312
|
+
let d;
|
|
313
|
+
try {
|
|
314
|
+
d = await loadData();
|
|
315
|
+
} catch (err) {
|
|
316
|
+
const e = toAppletError(err);
|
|
317
|
+
return refuseRecords("unreachable", e.message);
|
|
318
|
+
}
|
|
319
|
+
const entity = d.entities.get(alias);
|
|
320
|
+
if (entity !== void 0) return entityRefusal(alias, entity);
|
|
321
|
+
if (!d.port) return refuseRecords("not_found", `This Applet has no data source called "${alias}".`);
|
|
322
|
+
return call(d.port);
|
|
323
|
+
};
|
|
324
|
+
const data = {
|
|
325
|
+
subscribe(event, listener) {
|
|
326
|
+
let stop = null;
|
|
327
|
+
let stopped = false;
|
|
328
|
+
void loadData().then(
|
|
329
|
+
(d) => {
|
|
330
|
+
const alias = event.slice("rows:".length);
|
|
331
|
+
if (stopped || !d.port || d.entities.has(alias)) return;
|
|
332
|
+
stop = d.port.subscribe(event, listener);
|
|
333
|
+
},
|
|
334
|
+
(err) => report(toAppletError(err), `events.on(${event})`)
|
|
335
|
+
);
|
|
336
|
+
return () => {
|
|
337
|
+
stopped = true;
|
|
338
|
+
stop?.();
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
async sources() {
|
|
342
|
+
try {
|
|
343
|
+
const d = await loadData();
|
|
344
|
+
const tables = d.port ? await d.port.sources() : { ok: true, data: [] };
|
|
345
|
+
if (!tables.ok) return tables;
|
|
346
|
+
const ents = [...d.entities].map(([alias]) => ({ alias, label: alias, kind: "entity", canCreate: false, canUpdate: false }));
|
|
347
|
+
return { ok: true, data: [...tables.data, ...ents] };
|
|
348
|
+
} catch (err) {
|
|
349
|
+
return refuseRecords("unreachable", toAppletError(err).message);
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
columns: (alias) => routed(alias, (p) => p.columns(alias)),
|
|
353
|
+
list: (alias, q, cursor) => routed(alias, (p) => cursor === void 0 ? p.list(alias, q) : p.list(alias, q, cursor)),
|
|
354
|
+
get: (alias, id) => routed(alias, (p) => p.get(alias, id)),
|
|
355
|
+
create: (alias, v) => routed(alias, (p) => p.create(alias, v)),
|
|
356
|
+
update: (alias, id, patch, baseVersion) => routed(alias, (p) => p.update(alias, id, patch, baseVersion)),
|
|
357
|
+
archive: (alias, id) => routed(alias, (p) => p.archive(alias, id)),
|
|
358
|
+
restore: (alias, id) => routed(alias, (p) => p.restore(alias, id))
|
|
359
|
+
};
|
|
360
|
+
const keyFor = async (aliasOrKey) => {
|
|
361
|
+
try {
|
|
362
|
+
const rec = await record();
|
|
363
|
+
return rec.mandates.find((m) => m.alias === aliasOrKey)?.key ?? aliasOrKey;
|
|
364
|
+
} catch {
|
|
365
|
+
return aliasOrKey;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
const agents = opts.agents;
|
|
369
|
+
const intelligence = {
|
|
370
|
+
async describe(key) {
|
|
371
|
+
return agents.describe(await keyFor(key));
|
|
372
|
+
},
|
|
373
|
+
async run(req) {
|
|
374
|
+
let rec;
|
|
375
|
+
try {
|
|
376
|
+
rec = await record();
|
|
377
|
+
} catch (err) {
|
|
378
|
+
return { ok: false, error: toAppletError(err) };
|
|
379
|
+
}
|
|
380
|
+
const viewer = await loadViewer(world);
|
|
381
|
+
const organizationId = req.organizationId ?? opts.activeOrganizationId;
|
|
382
|
+
if (organizationId === null && !viewer.guest) {
|
|
383
|
+
return fail("organization_required", "Choose an organization to run this job in.");
|
|
384
|
+
}
|
|
385
|
+
return agents.run({
|
|
386
|
+
...req,
|
|
387
|
+
key: rec.mandates.find((m) => m.alias === req.key)?.key ?? req.key,
|
|
388
|
+
organizationId,
|
|
389
|
+
appletId: rec.id,
|
|
390
|
+
// the scope's own Applet, always
|
|
391
|
+
surfaceName: req.surfaceName ?? rec.surfaceName
|
|
392
|
+
});
|
|
393
|
+
},
|
|
394
|
+
result: (ref) => agents.result(ref),
|
|
395
|
+
answer: (ref, v) => agents.answer(ref, v),
|
|
396
|
+
rejoin: (ref) => agents.rejoin(ref),
|
|
397
|
+
async listRuns(key, scopedApplet) {
|
|
398
|
+
return agents.listRuns(await keyFor(key), scopedApplet ?? appletId);
|
|
399
|
+
},
|
|
400
|
+
createSoft: (spec) => agents.createSoft(spec)
|
|
401
|
+
};
|
|
402
|
+
const nav = {
|
|
403
|
+
async go(to) {
|
|
404
|
+
await opts.nav.go(to);
|
|
405
|
+
if (opts.nav.subscribe) return;
|
|
406
|
+
const location = await opts.nav.current();
|
|
407
|
+
for (const h of world.hosts.values()) h.emit(NAV_STREAM, location);
|
|
408
|
+
},
|
|
409
|
+
current: () => opts.nav.current()
|
|
410
|
+
};
|
|
411
|
+
const readChain = async (name, maxDepth) => {
|
|
412
|
+
const names = [];
|
|
413
|
+
let at = name;
|
|
414
|
+
while (at) {
|
|
415
|
+
if (names.includes(at)) return fail("surface_cycle", `The surface chain loops at "${at}": ${[...names, at].join(" \u2192 ")}.`);
|
|
416
|
+
if (names.length >= MAX_CHAIN) return fail("surface_too_deep", `The surface chain for "${name}" is deeper than ${MAX_CHAIN}.`);
|
|
417
|
+
const row = await read(
|
|
418
|
+
supabase.schema("ui").from("ui_surface").select("name, parent_surface_name").eq("name", at).is("deleted_at", null).maybeSingle(),
|
|
419
|
+
(d) => d
|
|
420
|
+
);
|
|
421
|
+
if (!row.ok) return row;
|
|
422
|
+
if (!isRecord(row.data)) {
|
|
423
|
+
return fail(
|
|
424
|
+
"surface_not_found",
|
|
425
|
+
names.length === 0 ? `The surface "${at}" does not exist yet; saving the Applet writes it (ui.save_applet_surface).` : `The surface "${names[names.length - 1]}" names a parent "${at}" that does not exist.`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
names.push(at);
|
|
429
|
+
at = typeof row.data.parent_surface_name === "string" && row.data.parent_surface_name ? row.data.parent_surface_name : null;
|
|
430
|
+
}
|
|
431
|
+
const rows = await read(
|
|
432
|
+
supabase.schema("ui").from("ui_surface_action").select("*").in("surface_name", names).is("deleted_at", null).order("sort_order", { ascending: true }),
|
|
433
|
+
(d) => Array.isArray(d) ? d : []
|
|
434
|
+
);
|
|
435
|
+
if (!rows.ok) return rows;
|
|
436
|
+
const chain = [...names].reverse().map((surfaceName) => ({
|
|
437
|
+
surfaceName,
|
|
438
|
+
actions: rows.data.filter((r) => r.surface_name === surfaceName).map(import_actions.surfaceActionRowToAction)
|
|
439
|
+
}));
|
|
440
|
+
try {
|
|
441
|
+
return ok((0, import_actions.resolveActionChain)(chain, maxDepth === void 0 ? {} : { maxDepth }));
|
|
442
|
+
} catch (err) {
|
|
443
|
+
return { ok: false, error: toAppletError(err, "surface_chain") };
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
const seedValues = async (rec) => {
|
|
447
|
+
const viewer = await loadViewer(world);
|
|
448
|
+
let location = null;
|
|
449
|
+
try {
|
|
450
|
+
location = await opts.nav.current();
|
|
451
|
+
} catch (err) {
|
|
452
|
+
report(toAppletError(err), "nav.current");
|
|
453
|
+
}
|
|
454
|
+
const seeds = {
|
|
455
|
+
viewer: { userId: viewer.userId, guest: viewer.guest },
|
|
456
|
+
organization: viewer.activeOrganizationId,
|
|
457
|
+
record: { id: rec.id, slug: rec.slug, version: rec.version },
|
|
458
|
+
sources: rec.sources.map((s) => s.alias),
|
|
459
|
+
route: location ? `/${location.path.join("/")}` : null,
|
|
460
|
+
page: location ? { path: location.path, params: location.params } : null
|
|
461
|
+
};
|
|
462
|
+
for (const [name, v] of Object.entries(seeds)) if (!values.has(name)) values.set(name, v);
|
|
463
|
+
};
|
|
464
|
+
const dispatchPage = async (name, input, context) => {
|
|
465
|
+
const declaredOn = context.surfaceName ?? "";
|
|
466
|
+
if (declaredOn === APPLET_BASE_SURFACE && BASE_PAGE_ACTIONS.has(name)) return builtIn(name, input);
|
|
467
|
+
let owner = self;
|
|
468
|
+
for (const h of world.hosts.values()) {
|
|
469
|
+
if (!h.hasSubscriber(PAGE_ACTION_STREAM)) continue;
|
|
470
|
+
const surfaceName = await h.surfaceName().catch(() => null);
|
|
471
|
+
if (surfaceName === declaredOn) owner = h;
|
|
472
|
+
}
|
|
473
|
+
if (!owner.hasSubscriber(PAGE_ACTION_STREAM)) {
|
|
474
|
+
return fail("action_unhandled", `No page is open to run "${name}".`);
|
|
475
|
+
}
|
|
476
|
+
return new Promise((resolve) => {
|
|
477
|
+
world.pending.set(context.runId, resolve);
|
|
478
|
+
const event = { action: name, input, runId: context.runId, by: context.by };
|
|
479
|
+
owner.emit(PAGE_ACTION_STREAM, event);
|
|
480
|
+
});
|
|
481
|
+
};
|
|
482
|
+
const builtIn = async (name, input) => {
|
|
483
|
+
if (name !== "write_row") return fail("action_unhandled", `No handler for "${name}".`);
|
|
484
|
+
if (!isRecord(input) || typeof input.source !== "string" || !isRecord(input.values)) {
|
|
485
|
+
return fail("invalid_argument", "write_row needs a source and values.");
|
|
486
|
+
}
|
|
487
|
+
let answer;
|
|
488
|
+
if (typeof input.id === "string") {
|
|
489
|
+
if (typeof input.baseVersion !== "number") {
|
|
490
|
+
return fail("invalid_argument", "write_row needs the row's baseVersion to change it, so a stale write is refused.");
|
|
491
|
+
}
|
|
492
|
+
answer = await data.update(input.source, input.id, input.values, input.baseVersion);
|
|
493
|
+
} else {
|
|
494
|
+
answer = await data.create(input.source, input.values);
|
|
495
|
+
}
|
|
496
|
+
return answer.ok ? ok(answer.data) : { ok: false, error: fromRecordsError(answer.error) };
|
|
497
|
+
};
|
|
498
|
+
const actionPorts = () => {
|
|
499
|
+
const diagnostics = {
|
|
500
|
+
capture(error, context) {
|
|
501
|
+
report({ ...toAppletError(error, "action_failed"), diagnostic: JSON.stringify(context.detail) }, `surface.${context.area}`);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
const s = opts.surfaces ?? {};
|
|
505
|
+
return {
|
|
506
|
+
diagnostics,
|
|
507
|
+
...s.serverActions ? { serverActions: s.serverActions } : {},
|
|
508
|
+
...s.approvals ? { approvals: s.approvals } : {},
|
|
509
|
+
...s.door ? { door: s.door } : {},
|
|
510
|
+
mandates: {
|
|
511
|
+
async run(request) {
|
|
512
|
+
const viewer = await loadViewer(world);
|
|
513
|
+
const started = await intelligence.run({
|
|
514
|
+
key: request.key ?? request.alias ?? "",
|
|
515
|
+
variables: request.variables,
|
|
516
|
+
...request.userInput !== void 0 ? { userInput: request.userInput } : {},
|
|
517
|
+
organizationId: viewer.activeOrganizationId
|
|
518
|
+
});
|
|
519
|
+
if (!started.ok) throw new Error(started.error.message);
|
|
520
|
+
return started.data;
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
pageActions: { dispatch: dispatchPage },
|
|
524
|
+
overlay: {
|
|
525
|
+
navigate(href) {
|
|
526
|
+
void nav.go(href).catch((err) => report(toAppletError(err), "nav.go"));
|
|
527
|
+
},
|
|
528
|
+
open(overlayId) {
|
|
529
|
+
report({ code: "not_supported", message: `Overlays cannot be opened from an Applet yet ("${overlayId}").`, retryable: false }, "surface.overlay");
|
|
530
|
+
},
|
|
531
|
+
close() {
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
};
|
|
536
|
+
const chainFor = async () => {
|
|
537
|
+
if (mounted) return ok(mounted.chain);
|
|
538
|
+
const rec = await record();
|
|
539
|
+
return readChain(rec.surfaceName, opts.surfaces?.maxDepth);
|
|
540
|
+
};
|
|
541
|
+
const surface = {
|
|
542
|
+
async mount(name) {
|
|
543
|
+
try {
|
|
544
|
+
const chain = await readChain(name, opts.surfaces?.maxDepth);
|
|
545
|
+
if (!chain.ok) return chain;
|
|
546
|
+
await seedValues(await record());
|
|
547
|
+
mounted = { mountId: mint(world, "mount"), name, chain: chain.data };
|
|
548
|
+
return ok({ mountId: mounted.mountId });
|
|
549
|
+
} catch (err) {
|
|
550
|
+
return { ok: false, error: toAppletError(err) };
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
async unmount(mountId) {
|
|
554
|
+
if (mounted?.mountId === mountId) mounted = null;
|
|
555
|
+
},
|
|
556
|
+
async getValue(name) {
|
|
557
|
+
return clone(values.get(name));
|
|
558
|
+
},
|
|
559
|
+
async setValue(name, v) {
|
|
560
|
+
values.set(name, clone(v));
|
|
561
|
+
emit(valueStream(name), v);
|
|
562
|
+
},
|
|
563
|
+
async listActions() {
|
|
564
|
+
try {
|
|
565
|
+
const chain = await chainFor();
|
|
566
|
+
if (!chain.ok) {
|
|
567
|
+
report(chain.error, "surface.listActions");
|
|
568
|
+
return [];
|
|
569
|
+
}
|
|
570
|
+
return clone(chain.data);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
report(toAppletError(err), "surface.listActions");
|
|
573
|
+
return [];
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
async runAction(name, input, by) {
|
|
577
|
+
let chain;
|
|
578
|
+
try {
|
|
579
|
+
chain = await chainFor();
|
|
580
|
+
} catch (err) {
|
|
581
|
+
return { ok: false, error: toAppletError(err) };
|
|
582
|
+
}
|
|
583
|
+
if (!chain.ok) return chain;
|
|
584
|
+
const info = chain.data.find((a) => a.name === name);
|
|
585
|
+
if (!info) return fail("action_not_found", `This page has no action named "${name}".`);
|
|
586
|
+
const viewer = await loadViewer(world);
|
|
587
|
+
const result = await (0, import_actions.runSurfaceAction)(info, input, by, actionPorts(), { runId: mint(world, "act"), viewerId: viewer.userId });
|
|
588
|
+
if (result.ok && info.updatesValue) await surface.setValue(info.updatesValue, result.data);
|
|
589
|
+
return result;
|
|
590
|
+
},
|
|
591
|
+
async completeAction(runId, result) {
|
|
592
|
+
const resolve = world.pending.get(runId);
|
|
593
|
+
world.pending.delete(runId);
|
|
594
|
+
resolve?.(result);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
const kindRow = async (slug) => read(
|
|
598
|
+
supabase.schema("content_ir").from("kind_definition").select("id, kind, label, data, sample_data, emitted_json_schema, variants, version").eq("kind", slug).is("deleted_at", null).maybeSingle(),
|
|
599
|
+
(d) => isRecord(d) ? d : null
|
|
600
|
+
);
|
|
601
|
+
const kinds = {
|
|
602
|
+
async definition(slug) {
|
|
603
|
+
const row = await kindRow(slug);
|
|
604
|
+
if (!row.ok) return row;
|
|
605
|
+
if (!row.data) return fail("kind_not_found", `No kind named "${slug}".`);
|
|
606
|
+
return ok(row.data);
|
|
607
|
+
},
|
|
608
|
+
async component(slug, platform) {
|
|
609
|
+
const row = await kindRow(slug);
|
|
610
|
+
if (!row.ok) return row;
|
|
611
|
+
if (!row.data) return fail("kind_not_found", `No kind named "${slug}".`);
|
|
612
|
+
const comp = await read(
|
|
613
|
+
supabase.schema("content_ir").from("kind_component").select("component_key, component_source, is_default, sort_order").eq("kind_definition_id", row.data.id).eq("platform", platform).eq("role", "output").eq("is_active", true).is("deleted_at", null).order("is_default", { ascending: false }).order("sort_order", { ascending: true }).limit(1),
|
|
614
|
+
(d) => Array.isArray(d) && isRecord(d[0]) ? d[0] : null
|
|
615
|
+
);
|
|
616
|
+
if (!comp.ok) return comp;
|
|
617
|
+
if (!comp.data || typeof comp.data.component_source !== "string") return ok(null);
|
|
618
|
+
const file = `${typeof comp.data.component_key === "string" && comp.data.component_key ? comp.data.component_key : slug}.tsx`;
|
|
619
|
+
return ok({ files: { [file]: comp.data.component_source }, entry: file });
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
const pages2 = {
|
|
623
|
+
async definition(pageId) {
|
|
624
|
+
return fail("not_supported", `Table-built pages cannot be opened from an Applet yet (page ${pageId}); there is no page store to read.`);
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
const self = {
|
|
628
|
+
kind: "platform",
|
|
629
|
+
appletId,
|
|
630
|
+
hostId,
|
|
631
|
+
viewer: () => loadViewer(world),
|
|
632
|
+
record,
|
|
633
|
+
async child(childId) {
|
|
634
|
+
childCount += 1;
|
|
635
|
+
const id = `${hostId}/${childId}#${childCount}`;
|
|
636
|
+
buildHost(world, childId, id, self);
|
|
637
|
+
return { hostId: id };
|
|
638
|
+
},
|
|
639
|
+
kinds,
|
|
640
|
+
pages: pages2,
|
|
641
|
+
data,
|
|
642
|
+
intelligence,
|
|
643
|
+
surface,
|
|
644
|
+
nav,
|
|
645
|
+
events: {
|
|
646
|
+
async on(stream) {
|
|
647
|
+
const subscriptionId = mint(world, "sub");
|
|
648
|
+
let stop = null;
|
|
649
|
+
if (stream.startsWith("rows:")) {
|
|
650
|
+
stop = data.subscribe(stream, (event) => deliver(subscriptionId, event));
|
|
651
|
+
} else if (stream.startsWith("run:")) {
|
|
652
|
+
stop = agents.events.on(stream, (event) => deliver(subscriptionId, event));
|
|
653
|
+
}
|
|
654
|
+
subscriptions.set(subscriptionId, { stream, stop });
|
|
655
|
+
return { subscriptionId };
|
|
656
|
+
},
|
|
657
|
+
async off(subscriptionId) {
|
|
658
|
+
subscriptions.get(subscriptionId)?.stop?.();
|
|
659
|
+
subscriptions.delete(subscriptionId);
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
async reportError(err) {
|
|
663
|
+
await opts.reportError(err);
|
|
664
|
+
},
|
|
665
|
+
receive(listener) {
|
|
666
|
+
listeners.add(listener);
|
|
667
|
+
return () => {
|
|
668
|
+
listeners.delete(listener);
|
|
669
|
+
};
|
|
670
|
+
},
|
|
671
|
+
forHost(id) {
|
|
672
|
+
return world.hosts.get(id) ?? null;
|
|
673
|
+
},
|
|
674
|
+
dispose() {
|
|
675
|
+
for (const s of subscriptions.values()) s.stop?.();
|
|
676
|
+
subscriptions.clear();
|
|
677
|
+
listeners.clear();
|
|
678
|
+
world.hosts.delete(hostId);
|
|
679
|
+
if (parent === null) {
|
|
680
|
+
for (const h of [...world.hosts.values()]) h.dispose();
|
|
681
|
+
world.stopNav?.();
|
|
682
|
+
world.stopNav = null;
|
|
683
|
+
for (const resolve of world.pending.values()) resolve(fail("host_disposed", "The page closed before the action finished."));
|
|
684
|
+
world.pending.clear();
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
emit,
|
|
688
|
+
hasSubscriber(stream) {
|
|
689
|
+
for (const s of subscriptions.values()) if (s.stream === stream) return true;
|
|
690
|
+
return false;
|
|
691
|
+
},
|
|
692
|
+
async surfaceName() {
|
|
693
|
+
return (await record()).surfaceName;
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
world.hosts.set(hostId, self);
|
|
697
|
+
return self;
|
|
698
|
+
}
|
|
699
|
+
//# sourceMappingURL=platform.cjs.map
|