@peers-app/peers-sdk 0.28.0 → 0.28.2
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/dist/contracts/host-contract-client.js +4 -2
- package/dist/data/orm/sql.data-source.js +16 -13
- package/dist/data/orm/table-container.js +6 -3
- package/dist/data/package-table-identity.d.ts +15 -0
- package/dist/data/package-table-identity.js +50 -14
- package/dist/data/package-version-resolver.js +14 -2
- package/dist/define/context.d.ts +12 -0
- package/dist/define/define-package.d.ts +21 -0
- package/dist/define/define-package.js +20 -0
- package/dist/define/index.d.ts +2 -1
- package/dist/define/index.js +2 -1
- package/dist/define/manifest.js +13 -2
- package/dist/define/serve.d.ts +10 -3
- package/dist/define/serve.js +28 -4
- package/dist/define/table-handle.d.ts +12 -3
- package/dist/define/table-handle.js +28 -10
- package/dist/define/table.d.ts +40 -3
- package/dist/define/table.js +32 -5
- package/dist/define/wire-value.d.ts +6 -1
- package/dist/define/wire-value.js +6 -3
- package/dist/logging/console-logger.d.ts +10 -1
- package/dist/logging/console-logger.js +17 -7
- package/dist/package-loader/isolated-artifact.d.ts +14 -0
- package/dist/package-loader/isolated-artifact.js +3 -1
- package/dist/package-loader/isolated-runtime.d.ts +29 -1
- package/dist/package-loader/isolated-runtime.js +13 -0
- package/dist/package-loader/isolated-table-gateway.js +4 -1
- package/dist/package-loader/package-loader.d.ts +9 -0
- package/dist/package-loader/package-loader.js +30 -0
- package/dist/peer-id.d.ts +19 -0
- package/dist/peer-id.js +41 -0
- package/dist/testing/mock-host-contract.js +2 -0
- package/dist/utils.js +13 -32
- package/package.json +1 -1
package/dist/define/table.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
2
|
import type { DataFilter, IDataQueryParams } from "../data/orm/data-query";
|
|
3
3
|
import type { IDataChangedEvent } from "../data/orm/subscribable.data-source";
|
|
4
|
-
import type
|
|
4
|
+
import { type IPackageSaveOptions } from "../package-loader/isolated-runtime";
|
|
5
|
+
import type { IPackageContext, PackageUnsubscribe } from "./context";
|
|
5
6
|
/** What {@link defineTable} takes. */
|
|
6
7
|
export interface IPackageTableDefinition<S extends z.AnyZodObject = z.AnyZodObject> {
|
|
7
8
|
/** Table name, unique in the package. Other packages reach it only through a contract. */
|
|
@@ -16,6 +17,12 @@ export interface IPackageTableDefinition<S extends z.AnyZodObject = z.AnyZodObje
|
|
|
16
17
|
versionNumber?: number;
|
|
17
18
|
/** Keep rows on this device only. They never sync. */
|
|
18
19
|
localOnly?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Indexes for the fields queries filter or sort by, each in order (a field name, or a name with
|
|
22
|
+
* `"ASC"` or `"DESC"`). A local-only table can also make one `unique`. Changing them is a
|
|
23
|
+
* schema change, so raise `versionNumber`.
|
|
24
|
+
*/
|
|
25
|
+
indexes?: IPackageTableIndex<S>[];
|
|
19
26
|
/**
|
|
20
27
|
* The table's class, to override its methods: `Feeds(ctx)` returns one, and so does every
|
|
21
28
|
* consumer call through a contract, so an override is always the logic in use. Defaults to
|
|
@@ -23,6 +30,15 @@ export interface IPackageTableDefinition<S extends z.AnyZodObject = z.AnyZodObje
|
|
|
23
30
|
*/
|
|
24
31
|
tableClass?: PackageTableClass<S>;
|
|
25
32
|
}
|
|
33
|
+
/** One index of a {@link defineTable} table: its fields in order, each ascending unless set. */
|
|
34
|
+
export interface IPackageTableIndex<S extends z.AnyZodObject = z.AnyZodObject> {
|
|
35
|
+
fields: (Extract<keyof z.infer<S>, string> | {
|
|
36
|
+
name: Extract<keyof z.infer<S>, string>;
|
|
37
|
+
order: "ASC" | "DESC";
|
|
38
|
+
})[];
|
|
39
|
+
/** No two rows share these values. Only on a `localOnly` table: synced rows can collide. */
|
|
40
|
+
unique?: true;
|
|
41
|
+
}
|
|
26
42
|
/** A class extending {@link PackageTable} for a table's schema. */
|
|
27
43
|
export type PackageTableClass<S extends z.AnyZodObject = z.AnyZodObject> = new (ctx: IPackageContext, definition: Readonly<IPackageTableDefinition<S>>) => PackageTable<S>;
|
|
28
44
|
/** One table's methods, bound to one handler call. */
|
|
@@ -38,8 +54,10 @@ export interface IPackageTableHandle<T extends Record<string, any>, TInput = T>
|
|
|
38
54
|
/**
|
|
39
55
|
* Insert or replace a row. It reaches the providing package's table, which parses it with the
|
|
40
56
|
* schema first (so defaults and transforms apply), through any override its class makes.
|
|
57
|
+
* Pass `{ restoreIfDeleted: true }` to save a row whose id was deleted before; without it,
|
|
58
|
+
* that save is refused.
|
|
41
59
|
*/
|
|
42
|
-
save(record: TInput): Promise<T>;
|
|
60
|
+
save(record: TInput, options?: IPackageSaveOptions): Promise<T>;
|
|
43
61
|
/** Delete a row by id. */
|
|
44
62
|
delete(recordId: string): Promise<void>;
|
|
45
63
|
}
|
|
@@ -88,10 +106,29 @@ export declare class PackageTable<S extends z.AnyZodObject = z.AnyZodObject> imp
|
|
|
88
106
|
* Insert or replace a row, parsed with the schema first so defaults and transforms apply. An
|
|
89
107
|
* optional field that is `null` or `undefined` is left out, so it's stored unset; a field with
|
|
90
108
|
* a default gets the default. Resolves to the row as stored.
|
|
109
|
+
*
|
|
110
|
+
* @param record - The row.
|
|
111
|
+
* @param options - `{ restoreIfDeleted: true }` saves a row whose id was deleted before;
|
|
112
|
+
* without it, that save is refused. An override that calls `super.save` passes it on.
|
|
91
113
|
*/
|
|
92
|
-
save(record: z.input<S
|
|
114
|
+
save(record: z.input<S>, options?: IPackageSaveOptions): Promise<z.infer<S>>;
|
|
93
115
|
/** Delete a row by id. */
|
|
94
116
|
delete(recordId: string): Promise<void>;
|
|
117
|
+
/**
|
|
118
|
+
* Call `handler` with each change to this table: inserts, updates, and deletes, made by this
|
|
119
|
+
* package, by a consumer, or synced from another device. Changes arrive in order, one at a
|
|
120
|
+
* time; the package's own writes from `handler` arrive too. A handler gets a tool call's
|
|
121
|
+
* deadline; past it, later changes stop waiting for it. Subscribe in `pkg.onStart`: it lasts
|
|
122
|
+
* until the returned function is called or the worker stops, and nothing is replayed from
|
|
123
|
+
* while the worker was stopped, so subscribe first, then read what's already there.
|
|
124
|
+
*
|
|
125
|
+
* Only the package itself can subscribe this way; consumers subscribe to `dataChanged`
|
|
126
|
+
* through a contract.
|
|
127
|
+
*
|
|
128
|
+
* @param handler - Gets each change, with the row as `list` returns it.
|
|
129
|
+
* @returns Stops the subscription.
|
|
130
|
+
*/
|
|
131
|
+
subscribe(handler: (event: IDataChangedEvent<z.infer<S>>) => unknown): Promise<PackageUnsubscribe>;
|
|
95
132
|
/**
|
|
96
133
|
* One change on its way to one subscriber of the table's `dataChanged`, with `this.ctx`
|
|
97
134
|
* carrying that subscriber. Return it to send it, a changed copy to send that instead, or
|
package/dist/define/table.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PackageTable = void 0;
|
|
4
4
|
exports.defineTable = defineTable;
|
|
5
|
+
const isolated_runtime_1 = require("../package-loader/isolated-runtime");
|
|
5
6
|
const empty_fields_1 = require("./empty-fields");
|
|
6
|
-
const
|
|
7
|
+
const table_handle_1 = require("./table-handle");
|
|
7
8
|
/**
|
|
8
9
|
* One of the package's tables, for one call: what `Feeds(ctx)` returns, and what answers a
|
|
9
10
|
* consumer's call through a contract (from a UI or another package). It runs in the provider's
|
|
@@ -40,7 +41,7 @@ class PackageTable {
|
|
|
40
41
|
}
|
|
41
42
|
/** Send one operation to storage and clean rows before provider code receives them. */
|
|
42
43
|
async store(operation, args) {
|
|
43
|
-
const result = await this.ctx.callOwnedTable(this.definition.name, operation, args);
|
|
44
|
+
const result = await this.ctx.callOwnedTable(this.definition.name, operation, (0, table_handle_1.bridgeTableArgs)(operation, args));
|
|
44
45
|
const { schema } = this.definition;
|
|
45
46
|
if (operation === "list" && Array.isArray(result)) {
|
|
46
47
|
return result.map((row) => (0, empty_fields_1.withoutEmptySchemaFields)(schema, row));
|
|
@@ -71,16 +72,42 @@ class PackageTable {
|
|
|
71
72
|
* Insert or replace a row, parsed with the schema first so defaults and transforms apply. An
|
|
72
73
|
* optional field that is `null` or `undefined` is left out, so it's stored unset; a field with
|
|
73
74
|
* a default gets the default. Resolves to the row as stored.
|
|
75
|
+
*
|
|
76
|
+
* @param record - The row.
|
|
77
|
+
* @param options - `{ restoreIfDeleted: true }` saves a row whose id was deleted before;
|
|
78
|
+
* without it, that save is refused. An override that calls `super.save` passes it on.
|
|
74
79
|
*/
|
|
75
|
-
async save(record) {
|
|
80
|
+
async save(record, options) {
|
|
76
81
|
const { schema } = this.definition;
|
|
77
|
-
const row =
|
|
78
|
-
|
|
82
|
+
const row = schema.parse((0, empty_fields_1.withoutEmptySchemaFields)(schema, record));
|
|
83
|
+
const allowed = (0, isolated_runtime_1.isolatedSaveOptions)(options);
|
|
84
|
+
return (await this.store("save", allowed ? [row, allowed] : [row]));
|
|
79
85
|
}
|
|
80
86
|
/** Delete a row by id. */
|
|
81
87
|
async delete(recordId) {
|
|
82
88
|
await this.store("delete", [recordId]);
|
|
83
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Call `handler` with each change to this table: inserts, updates, and deletes, made by this
|
|
92
|
+
* package, by a consumer, or synced from another device. Changes arrive in order, one at a
|
|
93
|
+
* time; the package's own writes from `handler` arrive too. A handler gets a tool call's
|
|
94
|
+
* deadline; past it, later changes stop waiting for it. Subscribe in `pkg.onStart`: it lasts
|
|
95
|
+
* until the returned function is called or the worker stops, and nothing is replayed from
|
|
96
|
+
* while the worker was stopped, so subscribe first, then read what's already there.
|
|
97
|
+
*
|
|
98
|
+
* Only the package itself can subscribe this way; consumers subscribe to `dataChanged`
|
|
99
|
+
* through a contract.
|
|
100
|
+
*
|
|
101
|
+
* @param handler - Gets each change, with the row as `list` returns it.
|
|
102
|
+
* @returns Stops the subscription.
|
|
103
|
+
*/
|
|
104
|
+
async subscribe(handler) {
|
|
105
|
+
const { name, schema } = this.definition;
|
|
106
|
+
return this.ctx.watchOwnedTable(name, (event) => handler({
|
|
107
|
+
...event,
|
|
108
|
+
dataObject: (0, empty_fields_1.withoutEmptySchemaFields)(schema, event.dataObject),
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
84
111
|
/**
|
|
85
112
|
* One change on its way to one subscriber of the table's `dataChanged`, with `this.ctx`
|
|
86
113
|
* carrying that subscriber. Return it to send it, a changed copy to send that instead, or
|
|
@@ -8,5 +8,10 @@
|
|
|
8
8
|
* environments (jsdom) lack.
|
|
9
9
|
*
|
|
10
10
|
* @param value - The value to send: arguments, a result, a row, an event, or an observable value.
|
|
11
|
+
* @param options - `undefinedFieldsAsNull` sends a missing field as `null` instead of leaving it
|
|
12
|
+
* out, for a table filter: there `{ tag: undefined }` matches an empty field, as `null` does,
|
|
13
|
+
* and leaving it out would match every row.
|
|
11
14
|
*/
|
|
12
|
-
export declare function snapshotPackageValue(value: unknown
|
|
15
|
+
export declare function snapshotPackageValue(value: unknown, options?: {
|
|
16
|
+
undefinedFieldsAsNull?: boolean;
|
|
17
|
+
}): unknown;
|
|
@@ -11,8 +11,11 @@ exports.snapshotPackageValue = snapshotPackageValue;
|
|
|
11
11
|
* environments (jsdom) lack.
|
|
12
12
|
*
|
|
13
13
|
* @param value - The value to send: arguments, a result, a row, an event, or an observable value.
|
|
14
|
+
* @param options - `undefinedFieldsAsNull` sends a missing field as `null` instead of leaving it
|
|
15
|
+
* out, for a table filter: there `{ tag: undefined }` matches an empty field, as `null` does,
|
|
16
|
+
* and leaving it out would match every row.
|
|
14
17
|
*/
|
|
15
|
-
function snapshotPackageValue(value) {
|
|
18
|
+
function snapshotPackageValue(value, options = {}) {
|
|
16
19
|
const ancestors = new Set();
|
|
17
20
|
const copy = (current, path) => {
|
|
18
21
|
if (current === null || typeof current === "string" || typeof current === "boolean") {
|
|
@@ -56,10 +59,10 @@ function snapshotPackageValue(value) {
|
|
|
56
59
|
}
|
|
57
60
|
const record = {};
|
|
58
61
|
for (const [key, field] of Object.entries(current)) {
|
|
59
|
-
if (field !== undefined) {
|
|
62
|
+
if (field !== undefined || options.undefinedFieldsAsNull) {
|
|
60
63
|
// Define rather than assign, so a "__proto__" key stays a field.
|
|
61
64
|
Object.defineProperty(record, key, {
|
|
62
|
-
value: copy(field, `${path}.${key}`),
|
|
65
|
+
value: field === undefined ? null : copy(field, `${path}.${key}`),
|
|
63
66
|
enumerable: true,
|
|
64
67
|
writable: true,
|
|
65
68
|
configurable: true,
|
|
@@ -27,9 +27,18 @@ export declare function extractCoreMessage(message: string): string;
|
|
|
27
27
|
*/
|
|
28
28
|
export declare class Logger {
|
|
29
29
|
private source;
|
|
30
|
+
private process;
|
|
30
31
|
private pendingLogs;
|
|
31
32
|
private waitingForConsoleLogsTable;
|
|
32
|
-
|
|
33
|
+
/**
|
|
34
|
+
* @param source - What the lines are about, such as a module or package name. Defaults to the
|
|
35
|
+
* caller's file.
|
|
36
|
+
* @param options - `process` records the lines under another process's name (such as
|
|
37
|
+
* `"worker"` for a package worker's output) instead of this one's.
|
|
38
|
+
*/
|
|
39
|
+
constructor(source?: string, options?: {
|
|
40
|
+
process?: string;
|
|
41
|
+
});
|
|
33
42
|
/**
|
|
34
43
|
* Log a debug message
|
|
35
44
|
*/
|
|
@@ -199,9 +199,11 @@ function extractSource(stackTrace) {
|
|
|
199
199
|
return undefined;
|
|
200
200
|
}
|
|
201
201
|
/**
|
|
202
|
-
* Write a log entry to the ConsoleLogs table with a specific source
|
|
202
|
+
* Write a log entry to the ConsoleLogs table with a specific source. `processName` names another
|
|
203
|
+
* process the line came from (such as a package worker); its stack is not this one's, so an
|
|
204
|
+
* error gets none.
|
|
203
205
|
*/
|
|
204
|
-
async function writeLogToDatabaseWithSource(level, source, args) {
|
|
206
|
+
async function writeLogToDatabaseWithSource(level, source, args, processName) {
|
|
205
207
|
try {
|
|
206
208
|
const timestamp = (0, utils_1.getTimestamp)();
|
|
207
209
|
// Format message from arguments
|
|
@@ -223,12 +225,12 @@ async function writeLogToDatabaseWithSource(level, source, args) {
|
|
|
223
225
|
}
|
|
224
226
|
const context = args.length === 0 ? undefined : args.length === 1 ? args[0] : args;
|
|
225
227
|
// Get stack trace for errors
|
|
226
|
-
const stackTrace = level === "error" ? getStackTrace() : undefined;
|
|
228
|
+
const stackTrace = level === "error" && !processName ? getStackTrace() : undefined;
|
|
227
229
|
const logRecord = {
|
|
228
230
|
logId: (0, utils_1.newid)(),
|
|
229
231
|
timestamp,
|
|
230
232
|
level,
|
|
231
|
-
process: currentProcessName,
|
|
233
|
+
process: processName ?? currentProcessName,
|
|
232
234
|
processInstanceId: currentProcessInstanceId,
|
|
233
235
|
source,
|
|
234
236
|
message,
|
|
@@ -254,15 +256,23 @@ async function writeLogToDatabaseWithSource(level, source, args) {
|
|
|
254
256
|
*/
|
|
255
257
|
class Logger {
|
|
256
258
|
source;
|
|
259
|
+
process;
|
|
257
260
|
pendingLogs = [];
|
|
258
261
|
waitingForConsoleLogsTable = true;
|
|
259
|
-
|
|
262
|
+
/**
|
|
263
|
+
* @param source - What the lines are about, such as a module or package name. Defaults to the
|
|
264
|
+
* caller's file.
|
|
265
|
+
* @param options - `process` records the lines under another process's name (such as
|
|
266
|
+
* `"worker"` for a package worker's output) instead of this one's.
|
|
267
|
+
*/
|
|
268
|
+
constructor(source, options = {}) {
|
|
260
269
|
// If no source provided, try to extract from caller's location
|
|
261
270
|
if (!source) {
|
|
262
271
|
const stack = new Error().stack;
|
|
263
272
|
source = extractSource(stack) || "unknown";
|
|
264
273
|
}
|
|
265
274
|
this.source = source;
|
|
275
|
+
this.process = options.process;
|
|
266
276
|
(0, console_logs_table_1.ConsoleLogs)().then(() => {
|
|
267
277
|
this.waitingForConsoleLogsTable = false;
|
|
268
278
|
this.flushPendingLogs();
|
|
@@ -321,7 +331,7 @@ class Logger {
|
|
|
321
331
|
return;
|
|
322
332
|
}
|
|
323
333
|
// System is initialized, write directly
|
|
324
|
-
writeLogToDatabaseWithSource(level, this.source, args).catch((err) => {
|
|
334
|
+
writeLogToDatabaseWithSource(level, this.source, args, this.process).catch((err) => {
|
|
325
335
|
originalConsole.error("[console-logger] Failed to write log:", err);
|
|
326
336
|
});
|
|
327
337
|
}
|
|
@@ -330,7 +340,7 @@ class Logger {
|
|
|
330
340
|
this.pendingLogs.length = 0;
|
|
331
341
|
for (const { level, args } of _pendingLogs) {
|
|
332
342
|
await (0, utils_1.sleep)(10); // small delay to avoid stampede
|
|
333
|
-
await writeLogToDatabaseWithSource(level, this.source, args).catch((err) => {
|
|
343
|
+
await writeLogToDatabaseWithSource(level, this.source, args, this.process).catch((err) => {
|
|
334
344
|
originalConsole.error("[console-logger] Failed to flush pending log:", err);
|
|
335
345
|
});
|
|
336
346
|
}
|
|
@@ -125,6 +125,20 @@ export interface IIsolatedPackageTable {
|
|
|
125
125
|
* of a contract's table shape.
|
|
126
126
|
*/
|
|
127
127
|
localOnly?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Indexes for the fields queries filter or sort by, each in order. Unique only on a local-only
|
|
130
|
+
* table. Storage policy, like `localOnly`: changing them needs a higher `versionNumber`.
|
|
131
|
+
*/
|
|
132
|
+
indexes?: IIsolatedTableIndex[];
|
|
133
|
+
}
|
|
134
|
+
/** One index of an isolated package table: field names in order, each ascending unless set. */
|
|
135
|
+
export interface IIsolatedTableIndex {
|
|
136
|
+
fields: (string | {
|
|
137
|
+
name: string;
|
|
138
|
+
order: "ASC" | "DESC";
|
|
139
|
+
})[];
|
|
140
|
+
/** No two rows share these values. Local-only tables only. */
|
|
141
|
+
unique?: true;
|
|
128
142
|
}
|
|
129
143
|
/** Name-only reference that exposes one package table through a contract. */
|
|
130
144
|
export interface IIsolatedContractTableRef {
|
|
@@ -19,6 +19,7 @@ exports.findPackageTable = findPackageTable;
|
|
|
19
19
|
exports.toConsumedContracts = toConsumedContracts;
|
|
20
20
|
const contract_definition_schema_1 = require("../contracts/contract-definition-schema");
|
|
21
21
|
const access_level_1 = require("../data/access-level");
|
|
22
|
+
const package_table_identity_1 = require("../data/package-table-identity");
|
|
22
23
|
const persistent_var_identity_1 = require("../data/persistent-var-identity");
|
|
23
24
|
const field_type_1 = require("../types/field-type");
|
|
24
25
|
const utils_1 = require("../utils");
|
|
@@ -300,7 +301,7 @@ function validatePackageTables(manifest, strict) {
|
|
|
300
301
|
if (forbiddenIdentity) {
|
|
301
302
|
throw new Error(`${tableLabel} must not declare ${forbiddenIdentity}`);
|
|
302
303
|
}
|
|
303
|
-
assertOnlyKeys(table, ["name", "description", "primaryKeyName", "fields", "versionNumber", "localOnly"], tableLabel);
|
|
304
|
+
assertOnlyKeys(table, ["name", "description", "primaryKeyName", "fields", "versionNumber", "localOnly", "indexes"], tableLabel);
|
|
304
305
|
if (typeof table.name !== "string" || table.name.trim() === "") {
|
|
305
306
|
throw new Error(`${tableLabel}.name is required`);
|
|
306
307
|
}
|
|
@@ -336,6 +337,7 @@ function validatePackageTables(manifest, strict) {
|
|
|
336
337
|
else {
|
|
337
338
|
table.versionNumber = 1;
|
|
338
339
|
}
|
|
340
|
+
(0, package_table_identity_1.validateTableIndexes)(table.indexes, new Set(table.fields.map((field) => field.name)), `${tableLabel}.indexes`, table.localOnly);
|
|
339
341
|
}
|
|
340
342
|
}
|
|
341
343
|
function validateProvidedContract(contract, index, packageTables, guestApi, strict) {
|
|
@@ -4,12 +4,34 @@ import type { IContractCall, IContractProviderEndpoint } from "../contracts/cont
|
|
|
4
4
|
import type { ContractRegistry } from "../contracts/registry";
|
|
5
5
|
import type { PersistentVarScope } from "../data/persistent-vars";
|
|
6
6
|
import type { IField } from "../types/field-type";
|
|
7
|
-
import type { IIsolatedContractToolRef, IIsolatedInstalledPackage, IIsolatedInstallResult, IIsolatedPackageArtifact } from "./isolated-artifact";
|
|
7
|
+
import type { IIsolatedContractToolRef, IIsolatedInstalledPackage, IIsolatedInstallResult, IIsolatedPackageArtifact, IIsolatedTableIndex } from "./isolated-artifact";
|
|
8
8
|
import type { IPackageWorkerHealth } from "./package-runtime-state";
|
|
9
9
|
/** Standard table methods exposed through isolated contract-table references. */
|
|
10
10
|
export declare const ISOLATED_CONTRACT_TABLE_OPERATIONS: readonly ["get", "list", "count", "findOne", "save", "insert", "update", "delete"];
|
|
11
11
|
/** Consumer-callable operation on an isolated contract table. */
|
|
12
12
|
export type IsolatedContractTableOperation = (typeof ISOLATED_CONTRACT_TABLE_OPERATIONS)[number];
|
|
13
|
+
/** Options a package may pass with a table `save`. */
|
|
14
|
+
export interface IPackageSaveOptions {
|
|
15
|
+
/** Save a row whose id was deleted before. Without it, that save is refused. */
|
|
16
|
+
restoreIfDeleted?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The save options a package may use, from whatever a caller passed: only `restoreIfDeleted`.
|
|
20
|
+
* The host's other save options (snapshots, weak inserts) stay out of package hands.
|
|
21
|
+
*
|
|
22
|
+
* @param options - A table `save`'s second argument, as received.
|
|
23
|
+
* @returns The allowed options, or `undefined` when none are set.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isolatedSaveOptions(options: unknown): IPackageSaveOptions | undefined;
|
|
26
|
+
/** One line a package worker wrote to its stdout or stderr. */
|
|
27
|
+
export interface IIsolatedWorkerOutput {
|
|
28
|
+
/** The worker's package. */
|
|
29
|
+
packageId: string;
|
|
30
|
+
/** Which stream the line came from. */
|
|
31
|
+
stream: "stdout" | "stderr";
|
|
32
|
+
/** The line, without its newline. */
|
|
33
|
+
line: string;
|
|
34
|
+
}
|
|
13
35
|
/**
|
|
14
36
|
* Trusted caller identity for an isolated tool call. Never read from guest
|
|
15
37
|
* payloads — the host derives this from `DataContext` / workflow state.
|
|
@@ -165,6 +187,8 @@ export interface IIsolatedPackageTableDeclaration extends IIsolatedPackageTableR
|
|
|
165
187
|
versionNumber?: number;
|
|
166
188
|
/** Rows stay on the writing device; see `IIsolatedPackageTable.localOnly`. */
|
|
167
189
|
localOnly?: boolean;
|
|
190
|
+
/** Indexes; see `IIsolatedPackageTable.indexes`. */
|
|
191
|
+
indexes?: IIsolatedTableIndex[];
|
|
168
192
|
}
|
|
169
193
|
/**
|
|
170
194
|
* Trusted host capability for isolated-package table persistence.
|
|
@@ -301,6 +325,10 @@ export interface IIsolatedPackageRuntimeFactoryOptions {
|
|
|
301
325
|
persistentVarGateway?: IIsolatedPackagePersistentVarGateway;
|
|
302
326
|
/** Gateway for consumed calls to selected host/system providers. */
|
|
303
327
|
providerGateway?: IIsolatedContractProviderGateway;
|
|
328
|
+
/** Receives each line package workers write; without it, lines go to this process's output. */
|
|
329
|
+
onWorkerOutput?: (output: IIsolatedWorkerOutput) => void;
|
|
330
|
+
/** This device's id, which package code reads as `ctx.deviceId`. */
|
|
331
|
+
getDeviceId?: () => string | undefined;
|
|
304
332
|
}
|
|
305
333
|
/**
|
|
306
334
|
* Host callback that builds an {@link IIsolatedPackageRuntime} for one
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ISOLATED_CONTRACT_TABLE_EVENTS = exports.ISOLATED_CONTRACT_TABLE_OPERATIONS = void 0;
|
|
4
|
+
exports.isolatedSaveOptions = isolatedSaveOptions;
|
|
4
5
|
exports.setIsolatedPackageRuntimeFactory = setIsolatedPackageRuntimeFactory;
|
|
5
6
|
exports.getIsolatedPackageRuntimeFactory = getIsolatedPackageRuntimeFactory;
|
|
6
7
|
/** Standard table methods exposed through isolated contract-table references. */
|
|
@@ -14,6 +15,18 @@ exports.ISOLATED_CONTRACT_TABLE_OPERATIONS = [
|
|
|
14
15
|
"update",
|
|
15
16
|
"delete",
|
|
16
17
|
];
|
|
18
|
+
/**
|
|
19
|
+
* The save options a package may use, from whatever a caller passed: only `restoreIfDeleted`.
|
|
20
|
+
* The host's other save options (snapshots, weak inserts) stay out of package hands.
|
|
21
|
+
*
|
|
22
|
+
* @param options - A table `save`'s second argument, as received.
|
|
23
|
+
* @returns The allowed options, or `undefined` when none are set.
|
|
24
|
+
*/
|
|
25
|
+
function isolatedSaveOptions(options) {
|
|
26
|
+
return options?.restoreIfDeleted === true
|
|
27
|
+
? { restoreIfDeleted: true }
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
17
30
|
/** Table events consumers may subscribe to on an isolated contract table. */
|
|
18
31
|
exports.ISOLATED_CONTRACT_TABLE_EVENTS = ["dataChanged"];
|
|
19
32
|
let isolatedRuntimeFactory;
|
|
@@ -43,6 +43,7 @@ function validateDeclaration(declaration) {
|
|
|
43
43
|
if (declaration.localOnly !== undefined && typeof declaration.localOnly !== "boolean") {
|
|
44
44
|
throw new Error("Isolated table localOnly must be a boolean");
|
|
45
45
|
}
|
|
46
|
+
(0, package_table_identity_1.validateTableIndexes)(declaration.indexes, fieldNames, "Isolated table indexes", declaration.localOnly);
|
|
46
47
|
const { localOnly, ...rest } = declaration;
|
|
47
48
|
return {
|
|
48
49
|
...rest,
|
|
@@ -61,8 +62,9 @@ function toMetaData(rawDeclaration) {
|
|
|
61
62
|
primaryKeyName: declaration.primaryKeyName,
|
|
62
63
|
fields: declaration.fields,
|
|
63
64
|
versionNumber: versionNumber(declaration),
|
|
64
|
-
// Only when set: stored definitions hash their metadata, and older ones have no such
|
|
65
|
+
// Only when set: stored definitions hash their metadata, and older ones have no such keys.
|
|
65
66
|
...(declaration.localOnly ? { localOnly: true } : {}),
|
|
67
|
+
...(declaration.indexes?.length ? { indexes: declaration.indexes } : {}),
|
|
66
68
|
};
|
|
67
69
|
}
|
|
68
70
|
function normalizedMetaData(metaData) {
|
|
@@ -111,6 +113,7 @@ function declarationFromMetaData(metaData) {
|
|
|
111
113
|
fields: metaData.fields,
|
|
112
114
|
versionNumber: metaData.versionNumber,
|
|
113
115
|
localOnly: metaData.localOnly,
|
|
116
|
+
...(metaData.indexes?.length ? { indexes: metaData.indexes } : {}),
|
|
114
117
|
});
|
|
115
118
|
}
|
|
116
119
|
function metaDataFromStoredDefinition(record, incoming) {
|
|
@@ -48,6 +48,10 @@ export declare class PackageLoader {
|
|
|
48
48
|
private contractInstalledPackages;
|
|
49
49
|
/** Packages installed through the isolated runtime (no host eval). */
|
|
50
50
|
private isolatedPackageIds;
|
|
51
|
+
/** Each isolated package's name, for its worker's log lines. */
|
|
52
|
+
private isolatedPackageNames;
|
|
53
|
+
/** One logger per package whose worker wrote output, writing as process `worker`. */
|
|
54
|
+
private workerLoggers;
|
|
51
55
|
/** Per-package counter bumped on every runtime-state change; lets slow unloads yield. */
|
|
52
56
|
private runtimeStateSequences;
|
|
53
57
|
/** Hash of the bundle each loaded package was built from, for `onlyIfChanged` loads. */
|
|
@@ -189,6 +193,11 @@ export declare class PackageLoader {
|
|
|
189
193
|
* Dispose the isolated runtime for this data context.
|
|
190
194
|
*/
|
|
191
195
|
dispose(): Promise<void>;
|
|
196
|
+
/**
|
|
197
|
+
* Record one line a package worker wrote, under process `worker` with the package's name as
|
|
198
|
+
* its source. It still reaches this process's output too.
|
|
199
|
+
*/
|
|
200
|
+
private logWorkerOutput;
|
|
192
201
|
private retireHostEndpoints;
|
|
193
202
|
/**
|
|
194
203
|
* Resolve and load every non-disabled package in this data context.
|
|
@@ -16,6 +16,7 @@ const package_version_resolver_1 = require("../data/package-version-resolver");
|
|
|
16
16
|
const package_versions_1 = require("../data/package-versions");
|
|
17
17
|
const packages_1 = require("../data/packages");
|
|
18
18
|
const keys_1 = require("../keys");
|
|
19
|
+
const console_logger_1 = require("../logging/console-logger");
|
|
19
20
|
const observable_1 = require("../observable");
|
|
20
21
|
const contract_package_loader_1 = require("./contract-package-loader");
|
|
21
22
|
const isolated_artifact_1 = require("./isolated-artifact");
|
|
@@ -63,6 +64,10 @@ class PackageLoader {
|
|
|
63
64
|
contractInstalledPackages = new Set();
|
|
64
65
|
/** Packages installed through the isolated runtime (no host eval). */
|
|
65
66
|
isolatedPackageIds = new Set();
|
|
67
|
+
/** Each isolated package's name, for its worker's log lines. */
|
|
68
|
+
isolatedPackageNames = new Map();
|
|
69
|
+
/** One logger per package whose worker wrote output, writing as process `worker`. */
|
|
70
|
+
workerLoggers = new Map();
|
|
66
71
|
/** Per-package counter bumped on every runtime-state change; lets slow unloads yield. */
|
|
67
72
|
runtimeStateSequences = new Map();
|
|
68
73
|
/** Hash of the bundle each loaded package was built from, for `onlyIfChanged` loads. */
|
|
@@ -120,6 +125,8 @@ class PackageLoader {
|
|
|
120
125
|
resolverOnly: true,
|
|
121
126
|
}),
|
|
122
127
|
},
|
|
128
|
+
onWorkerOutput: (output) => this.logWorkerOutput(output),
|
|
129
|
+
getDeviceId: () => this.dataContext.userContext?.deviceId(),
|
|
123
130
|
});
|
|
124
131
|
this.unsubscribePackageHealth = this.isolatedRuntime.subscribePackageHealth?.((packageId) => {
|
|
125
132
|
const previous = this.runtimeStates()[packageId];
|
|
@@ -623,9 +630,28 @@ class PackageLoader {
|
|
|
623
630
|
this.contractInstalledPackages.clear();
|
|
624
631
|
this.packageInstances = {};
|
|
625
632
|
this.loadedBundleHashes.clear();
|
|
633
|
+
this.workerLoggers.clear();
|
|
634
|
+
this.isolatedPackageNames.clear();
|
|
626
635
|
this.runtimeStateSequences.clear();
|
|
627
636
|
this.runtimeStates({});
|
|
628
637
|
}
|
|
638
|
+
/**
|
|
639
|
+
* Record one line a package worker wrote, under process `worker` with the package's name as
|
|
640
|
+
* its source. It still reaches this process's output too.
|
|
641
|
+
*/
|
|
642
|
+
logWorkerOutput({ packageId, stream, line }) {
|
|
643
|
+
let logger = this.workerLoggers.get(packageId);
|
|
644
|
+
if (!logger) {
|
|
645
|
+
logger = new console_logger_1.Logger(this.isolatedPackageNames.get(packageId) ?? packageId, {
|
|
646
|
+
process: "worker",
|
|
647
|
+
});
|
|
648
|
+
this.workerLoggers.set(packageId, logger);
|
|
649
|
+
}
|
|
650
|
+
if (stream === "stderr")
|
|
651
|
+
logger.error(line);
|
|
652
|
+
else
|
|
653
|
+
logger.log(line);
|
|
654
|
+
}
|
|
629
655
|
retireHostEndpoints(packageId) {
|
|
630
656
|
this.hostActivationGenerations.set(packageId, (this.hostActivationGenerations.get(packageId) ?? 0) + 1);
|
|
631
657
|
for (const router of this.consumedContractRouters)
|
|
@@ -935,6 +961,10 @@ class PackageLoader {
|
|
|
935
961
|
return undefined;
|
|
936
962
|
}
|
|
937
963
|
assertCurrent();
|
|
964
|
+
if (this.isolatedPackageNames.get(pkg.packageId) !== pkg.name) {
|
|
965
|
+
this.workerLoggers.delete(pkg.packageId);
|
|
966
|
+
}
|
|
967
|
+
this.isolatedPackageNames.set(pkg.packageId, pkg.name);
|
|
938
968
|
const installOptions = { ...isolatedContractLifecycle(selectedPackageVersion), assertCurrent };
|
|
939
969
|
let installed;
|
|
940
970
|
if (this.isolatedRuntime.isInstalled(pkg.packageId)) {
|
package/dist/peer-id.d.ts
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
/** Builds a random string of base-36 characters with unbiased distribution (tweetnacl-backed). */
|
|
2
2
|
export declare function cryptoRandomString(length: number): string;
|
|
3
|
+
/**
|
|
4
|
+
* A 25-character peer id derived from `input`: `prefix`, then base-36 characters of its SHA-512
|
|
5
|
+
* hash. The same input always gives the same id. Each kind of derived id has its own prefix, so
|
|
6
|
+
* kinds can't collide.
|
|
7
|
+
*
|
|
8
|
+
* @param prefix - Fixed leading characters naming the kind of id.
|
|
9
|
+
* @param input - What the id is derived from.
|
|
10
|
+
*/
|
|
11
|
+
export declare function deterministicPeerId(prefix: string, input: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* A peer id derived from `key`, the same on every device. Rows different devices save under the
|
|
14
|
+
* same key are then one row, which sync merges as ordinary updates: how a synced table keeps one
|
|
15
|
+
* row per key, where a unique index can't (two devices could each save a row the other's index
|
|
16
|
+
* refuses). Start the key with what it identifies (`` `store:${name}` ``) so ids for different
|
|
17
|
+
* things can't collide.
|
|
18
|
+
*
|
|
19
|
+
* @param key - What the row stands for.
|
|
20
|
+
*/
|
|
21
|
+
export declare function idFromKey(key: string): string;
|
|
3
22
|
/**
|
|
4
23
|
* Creates a new 25-character peer id: base-36 time prefix (10 chars) + random suffix (15 chars).
|
|
5
24
|
* Sort order correlates with creation time. Use {@link isid} to validate user-provided values.
|
package/dist/peer-id.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MAX_ID = exports.MIN_ID = void 0;
|
|
4
4
|
exports.cryptoRandomString = cryptoRandomString;
|
|
5
|
+
exports.deterministicPeerId = deterministicPeerId;
|
|
6
|
+
exports.idFromKey = idFromKey;
|
|
5
7
|
exports.newid = newid;
|
|
6
8
|
exports.isid = isid;
|
|
7
9
|
exports.idTime = idTime;
|
|
@@ -35,6 +37,45 @@ function cryptoRandomString(length) {
|
|
|
35
37
|
}
|
|
36
38
|
return s.substring(0, length);
|
|
37
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Converts a byte array to a base-36 string of the requested length, using unbiased modular reduction.
|
|
42
|
+
* Bytes >= 252 are skipped to avoid bias (same technique as {@link cryptoRandomString}).
|
|
43
|
+
*/
|
|
44
|
+
function bytesToBase36(bytes, length) {
|
|
45
|
+
let s = "";
|
|
46
|
+
for (let i = 0; i < bytes.length && s.length < length; i++) {
|
|
47
|
+
const n = bytes[i];
|
|
48
|
+
if (n >= 252)
|
|
49
|
+
continue;
|
|
50
|
+
s += (n % 36).toString(36);
|
|
51
|
+
}
|
|
52
|
+
return s.substring(0, length);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A 25-character peer id derived from `input`: `prefix`, then base-36 characters of its SHA-512
|
|
56
|
+
* hash. The same input always gives the same id. Each kind of derived id has its own prefix, so
|
|
57
|
+
* kinds can't collide.
|
|
58
|
+
*
|
|
59
|
+
* @param prefix - Fixed leading characters naming the kind of id.
|
|
60
|
+
* @param input - What the id is derived from.
|
|
61
|
+
*/
|
|
62
|
+
function deterministicPeerId(prefix, input) {
|
|
63
|
+
const hashSuffixLength = 25 - prefix.length;
|
|
64
|
+
const hash = nacl.hash(new TextEncoder().encode(input));
|
|
65
|
+
return prefix + bytesToBase36(hash, hashSuffixLength);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A peer id derived from `key`, the same on every device. Rows different devices save under the
|
|
69
|
+
* same key are then one row, which sync merges as ordinary updates: how a synced table keeps one
|
|
70
|
+
* row per key, where a unique index can't (two devices could each save a row the other's index
|
|
71
|
+
* refuses). Start the key with what it identifies (`` `store:${name}` ``) so ids for different
|
|
72
|
+
* things can't collide.
|
|
73
|
+
*
|
|
74
|
+
* @param key - What the row stands for.
|
|
75
|
+
*/
|
|
76
|
+
function idFromKey(key) {
|
|
77
|
+
return deterministicPeerId("000key00", key);
|
|
78
|
+
}
|
|
38
79
|
/**
|
|
39
80
|
* Creates a new 25-character peer id: base-36 time prefix (10 chars) + random suffix (15 chars).
|
|
40
81
|
* Sort order correlates with creation time. Use {@link isid} to validate user-provided values.
|
|
@@ -237,6 +237,8 @@ function mockProviderContext(contractId, tables) {
|
|
|
237
237
|
get(_target, property) {
|
|
238
238
|
if (property === "callOwnedTable")
|
|
239
239
|
return callOwnedTable;
|
|
240
|
+
if (property === "deviceId")
|
|
241
|
+
return "mock-host-device";
|
|
240
242
|
if (typeof property !== "string" || property === "then")
|
|
241
243
|
return undefined;
|
|
242
244
|
return () => {
|