@peers-app/peers-sdk 0.23.2 → 0.23.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context/data-context.d.ts +36 -5
- package/dist/context/data-context.js +44 -9
- package/dist/context/user-context.d.ts +44 -0
- package/dist/context/user-context.js +91 -12
- package/dist/contracts/__tests__/contract-provider-router.test.js +26 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +44 -0
- package/dist/contracts/__tests__/registry.test.js +104 -0
- package/dist/contracts/contract-provider-router.d.ts +21 -0
- package/dist/contracts/contract-provider-router.js +31 -5
- package/dist/contracts/contract-proxy.d.ts +3 -0
- package/dist/contracts/contract-proxy.js +3 -0
- package/dist/contracts/contract-resolvers.d.ts +21 -3
- package/dist/contracts/contract-resolvers.js +105 -32
- package/dist/contracts/index.d.ts +3 -3
- package/dist/contracts/registry.d.ts +59 -0
- package/dist/contracts/registry.js +155 -25
- package/dist/contracts/system-contracts.js +3 -1
- package/dist/data/group-share.d.ts +13 -0
- package/dist/data/group-share.js +32 -8
- package/dist/data/group-share.test.d.ts +1 -0
- package/dist/data/group-share.test.js +110 -0
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.js +1 -0
- package/dist/data/orm/types.d.ts +19 -0
- package/dist/data/orm/types.js +11 -0
- package/dist/data/package-table-identity.d.ts +46 -0
- package/dist/data/package-table-identity.js +71 -0
- package/dist/data/package-table-identity.test.d.ts +1 -0
- package/dist/data/package-table-identity.test.js +46 -0
- package/dist/data/package-version-resolver.d.ts +13 -0
- package/dist/data/package-version-resolver.js +142 -6
- package/dist/data/package-version-resolver.test.js +42 -0
- package/dist/data/persistent-var-identity.d.ts +37 -0
- package/dist/data/persistent-var-identity.js +69 -0
- package/dist/data/persistent-vars.d.ts +3 -5
- package/dist/data/persistent-vars.js +25 -13
- package/dist/data/persistent-vars.package-scope.test.d.ts +1 -0
- package/dist/data/persistent-vars.package-scope.test.js +27 -0
- package/dist/data/tools.d.ts +3 -3
- package/dist/group-invite/group-invite.types.d.ts +15 -5
- package/dist/package-loader/contract-package-loader.js +20 -8
- package/dist/package-loader/contract-package-loader.test.js +16 -0
- package/dist/package-loader/index.d.ts +4 -0
- package/dist/package-loader/index.js +4 -0
- package/dist/package-loader/isolated-artifact.d.ts +233 -0
- package/dist/package-loader/isolated-artifact.js +643 -0
- package/dist/package-loader/isolated-artifact.test.d.ts +1 -0
- package/dist/package-loader/isolated-artifact.test.js +270 -0
- package/dist/package-loader/isolated-persistent-var-gateway.d.ts +20 -0
- package/dist/package-loader/isolated-persistent-var-gateway.js +106 -0
- package/dist/package-loader/isolated-persistent-var-gateway.test.d.ts +1 -0
- package/dist/package-loader/isolated-persistent-var-gateway.test.js +56 -0
- package/dist/package-loader/isolated-runtime.d.ts +229 -0
- package/dist/package-loader/isolated-runtime.js +37 -0
- package/dist/package-loader/isolated-table-gateway.d.ts +16 -0
- package/dist/package-loader/isolated-table-gateway.js +375 -0
- package/dist/package-loader/isolated-table-gateway.test.d.ts +1 -0
- package/dist/package-loader/isolated-table-gateway.test.js +206 -0
- package/dist/package-loader/isolated-value.d.ts +14 -0
- package/dist/package-loader/isolated-value.js +63 -0
- package/dist/package-loader/package-loader.d.ts +123 -4
- package/dist/package-loader/package-loader.js +417 -46
- package/dist/package-loader/package-loader.test.js +478 -12
- package/dist/package-loader/resolve-consumed-contracts.d.ts +3 -3
- package/dist/package-loader/resolve-consumed-contracts.js +18 -10
- package/dist/utils.d.ts +21 -2
- package/dist/utils.js +46 -7
- package/package.json +1 -1
|
@@ -3,29 +3,60 @@ import { EventRegistry, type ITableDependencies, type ITableMetaData, Table, Tab
|
|
|
3
3
|
import type { DataSourceFactory } from "../data/orm/table-definitions.type";
|
|
4
4
|
import { PackageLoader } from "../package-loader/package-loader";
|
|
5
5
|
import type { UserContext } from "./user-context";
|
|
6
|
+
/**
|
|
7
|
+
* Per-user or per-group workspace: tables, events, and the package loader
|
|
8
|
+
* that installs both legacy and isolated packages into this context.
|
|
9
|
+
*
|
|
10
|
+
* `tableContainer` is created before `packageLoader` so isolated packages that
|
|
11
|
+
* declare provider-owned tables receive a host table gateway at construct time.
|
|
12
|
+
*/
|
|
6
13
|
export declare class DataContext {
|
|
7
14
|
readonly userContext: UserContext;
|
|
8
15
|
readonly groupId?: string | undefined;
|
|
16
|
+
private disposePromise?;
|
|
17
|
+
/** Factory that wraps the user-context data source with this group id. */
|
|
9
18
|
readonly dataSourceFactory: DataSourceFactory;
|
|
19
|
+
/** Live table registry for this personal or group context. */
|
|
10
20
|
readonly tableContainer: TableContainer;
|
|
21
|
+
/** Loads and unloads packages for this context, including isolated artifacts. */
|
|
11
22
|
readonly packageLoader: PackageLoader;
|
|
23
|
+
/** In-process event bus scoped to {@link dataContextId}. */
|
|
12
24
|
readonly eventRegistry: EventRegistry;
|
|
25
|
+
/** Personal user id, or group id when this context is group-scoped. */
|
|
13
26
|
readonly dataContextId: string;
|
|
27
|
+
/**
|
|
28
|
+
* @param userContext - Owning signed-in user.
|
|
29
|
+
* @param groupId - When set, this context is the named group; otherwise personal.
|
|
30
|
+
*/
|
|
14
31
|
constructor(userContext: UserContext, groupId?: string | undefined);
|
|
15
32
|
/**
|
|
16
|
-
*
|
|
33
|
+
* Mark this context as the user's default unless a caller pins another one.
|
|
17
34
|
*/
|
|
18
35
|
setAsDefault(): void;
|
|
19
36
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
37
|
+
* Build the dependency bag a `Table` constructor needs for this context.
|
|
38
|
+
*
|
|
39
|
+
* @param metaData - Table metadata used to open the data source.
|
|
40
|
+
* @param schema - Optional Zod schema for the table rows.
|
|
22
41
|
*/
|
|
23
42
|
createTableDependencies(metaData: ITableMetaData, schema?: z.AnyZodObject): ITableDependencies;
|
|
24
43
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
44
|
+
* Construct a table (or a custom `Table` subclass) bound to this context.
|
|
45
|
+
*
|
|
46
|
+
* @param metaData - Table metadata.
|
|
47
|
+
* @param schema - Optional Zod schema for the table rows.
|
|
48
|
+
* @param TableClass - Table implementation; defaults to {@link Table}.
|
|
27
49
|
*/
|
|
28
50
|
createTable<T extends {
|
|
29
51
|
[key: string]: any;
|
|
30
52
|
}>(metaData: ITableMetaData, schema?: z.AnyZodObject, TableClass?: new (metaData: ITableMetaData, deps: ITableDependencies) => Table<T>): Table<T>;
|
|
53
|
+
/**
|
|
54
|
+
* Dispose package runtimes and release event emitters owned by this context.
|
|
55
|
+
*
|
|
56
|
+
* Repeated calls share one promise so group-removal and application-shutdown
|
|
57
|
+
* cleanup can safely race.
|
|
58
|
+
*
|
|
59
|
+
* @returns A promise that settles after all context resources are released.
|
|
60
|
+
*/
|
|
61
|
+
dispose(): Promise<void>;
|
|
31
62
|
}
|
|
@@ -3,37 +3,55 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.DataContext = void 0;
|
|
4
4
|
const orm_1 = require("../data/orm");
|
|
5
5
|
const package_loader_1 = require("../package-loader/package-loader");
|
|
6
|
+
/**
|
|
7
|
+
* Per-user or per-group workspace: tables, events, and the package loader
|
|
8
|
+
* that installs both legacy and isolated packages into this context.
|
|
9
|
+
*
|
|
10
|
+
* `tableContainer` is created before `packageLoader` so isolated packages that
|
|
11
|
+
* declare provider-owned tables receive a host table gateway at construct time.
|
|
12
|
+
*/
|
|
6
13
|
class DataContext {
|
|
7
14
|
userContext;
|
|
8
15
|
groupId;
|
|
16
|
+
disposePromise;
|
|
17
|
+
/** Factory that wraps the user-context data source with this group id. */
|
|
9
18
|
dataSourceFactory;
|
|
19
|
+
/** Live table registry for this personal or group context. */
|
|
10
20
|
tableContainer;
|
|
21
|
+
/** Loads and unloads packages for this context, including isolated artifacts. */
|
|
11
22
|
packageLoader;
|
|
23
|
+
/** In-process event bus scoped to {@link dataContextId}. */
|
|
12
24
|
eventRegistry;
|
|
25
|
+
/** Personal user id, or group id when this context is group-scoped. */
|
|
13
26
|
dataContextId;
|
|
27
|
+
/**
|
|
28
|
+
* @param userContext - Owning signed-in user.
|
|
29
|
+
* @param groupId - When set, this context is the named group; otherwise personal.
|
|
30
|
+
*/
|
|
14
31
|
constructor(userContext, groupId) {
|
|
15
32
|
this.userContext = userContext;
|
|
16
33
|
this.groupId = groupId;
|
|
17
34
|
this.dataContextId = groupId || userContext.userId;
|
|
18
|
-
// Initialize dependencies in the correct order to avoid circular references
|
|
19
35
|
this.eventRegistry = new orm_1.EventRegistry(this.dataContextId);
|
|
20
|
-
this.packageLoader = new package_loader_1.PackageLoader(this);
|
|
21
|
-
// Set up data source factory with group wrapping
|
|
22
36
|
const baseFactory = userContext.dataSourceFactory;
|
|
23
37
|
this.dataSourceFactory = (metaData, schema) => baseFactory(metaData, schema, this.groupId);
|
|
24
|
-
// Set up table factory and table container
|
|
25
38
|
const tableFactory = (metaData, schema, TableClass) => this.createTable(metaData, schema, TableClass);
|
|
26
39
|
this.tableContainer = new orm_1.TableContainer(tableFactory, groupId);
|
|
40
|
+
// PackageLoader reads `tableContainer` in its constructor to wire isolated
|
|
41
|
+
// table persistence. Keep this last.
|
|
42
|
+
this.packageLoader = new package_loader_1.PackageLoader(this);
|
|
27
43
|
}
|
|
28
44
|
/**
|
|
29
|
-
*
|
|
45
|
+
* Mark this context as the user's default unless a caller pins another one.
|
|
30
46
|
*/
|
|
31
47
|
setAsDefault() {
|
|
32
48
|
this.userContext.defaultDataContext(this);
|
|
33
49
|
}
|
|
34
50
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
51
|
+
* Build the dependency bag a `Table` constructor needs for this context.
|
|
52
|
+
*
|
|
53
|
+
* @param metaData - Table metadata used to open the data source.
|
|
54
|
+
* @param schema - Optional Zod schema for the table rows.
|
|
37
55
|
*/
|
|
38
56
|
createTableDependencies(metaData, schema) {
|
|
39
57
|
return {
|
|
@@ -44,12 +62,29 @@ class DataContext {
|
|
|
44
62
|
};
|
|
45
63
|
}
|
|
46
64
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
65
|
+
* Construct a table (or a custom `Table` subclass) bound to this context.
|
|
66
|
+
*
|
|
67
|
+
* @param metaData - Table metadata.
|
|
68
|
+
* @param schema - Optional Zod schema for the table rows.
|
|
69
|
+
* @param TableClass - Table implementation; defaults to {@link Table}.
|
|
49
70
|
*/
|
|
50
71
|
createTable(metaData, schema, TableClass = orm_1.Table) {
|
|
51
72
|
const deps = this.createTableDependencies(metaData, schema);
|
|
52
73
|
return new TableClass(metaData, deps);
|
|
53
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Dispose package runtimes and release event emitters owned by this context.
|
|
77
|
+
*
|
|
78
|
+
* Repeated calls share one promise so group-removal and application-shutdown
|
|
79
|
+
* cleanup can safely race.
|
|
80
|
+
*
|
|
81
|
+
* @returns A promise that settles after all context resources are released.
|
|
82
|
+
*/
|
|
83
|
+
dispose() {
|
|
84
|
+
this.disposePromise ??= this.packageLoader.dispose().finally(() => {
|
|
85
|
+
this.eventRegistry.clear();
|
|
86
|
+
});
|
|
87
|
+
return this.disposePromise;
|
|
88
|
+
}
|
|
54
89
|
}
|
|
55
90
|
exports.DataContext = DataContext;
|
|
@@ -18,8 +18,11 @@ export declare class UserContext {
|
|
|
18
18
|
readonly loadingPromise: Promise<UserContext>;
|
|
19
19
|
private personalUserSubscription?;
|
|
20
20
|
private ownUserSubscription?;
|
|
21
|
+
private groupsSubscription?;
|
|
22
|
+
private observableSubscriptions;
|
|
21
23
|
private groupUserSyncs;
|
|
22
24
|
private _currentlyActiveGroupIdPVar?;
|
|
25
|
+
private disposed;
|
|
23
26
|
constructor(userId: string, dataSourceFactory: DataSourceFactory, ephemeral?: boolean | undefined);
|
|
24
27
|
private init;
|
|
25
28
|
/**
|
|
@@ -35,6 +38,13 @@ export declare class UserContext {
|
|
|
35
38
|
force?: boolean;
|
|
36
39
|
}): Promise<void>;
|
|
37
40
|
private loadGroupContexts;
|
|
41
|
+
private removeGroupDataContext;
|
|
42
|
+
/**
|
|
43
|
+
* Return the personal context, or the group context for `groupId`, creating
|
|
44
|
+
* the group context if this user context does not already hold it.
|
|
45
|
+
*
|
|
46
|
+
* @param groupId - Group id, or the current user id for the personal context.
|
|
47
|
+
*/
|
|
38
48
|
getDataContext(groupId: string): DataContext;
|
|
39
49
|
/** Wait for any pending `currentlyActiveGroupId` pvar write to be flushed to the DB. */
|
|
40
50
|
waitForActiveGroupPersistence(): Promise<void>;
|
|
@@ -44,6 +54,11 @@ export declare class UserContext {
|
|
|
44
54
|
* for the user context to be fully loaded before proceeding with standard data access
|
|
45
55
|
*/
|
|
46
56
|
private loadUserContextObservablesFromDB;
|
|
57
|
+
/**
|
|
58
|
+
* Load the signed-in user's `Users` row from the personal data context.
|
|
59
|
+
*
|
|
60
|
+
* @returns The cached personal user record, or `undefined` if it is missing.
|
|
61
|
+
*/
|
|
47
62
|
getMe(): Promise<{
|
|
48
63
|
name: string;
|
|
49
64
|
publicKey: string;
|
|
@@ -51,12 +66,41 @@ export declare class UserContext {
|
|
|
51
66
|
userId: string;
|
|
52
67
|
signature?: string | undefined;
|
|
53
68
|
} | undefined>;
|
|
69
|
+
/**
|
|
70
|
+
* Sign and persist this user's identity, then copy it into every loaded
|
|
71
|
+
* group context and copy group records back into the personal context.
|
|
72
|
+
*
|
|
73
|
+
* @param userId - Signed-in user id.
|
|
74
|
+
* @param keys - Current public/private keypair used to sign the user row.
|
|
75
|
+
* @param me - Optional preloaded personal user row.
|
|
76
|
+
*/
|
|
54
77
|
syncUserAndGroupObjects(userId: string, keys: IPublicPrivateKeys, me?: IUser): Promise<void>;
|
|
55
78
|
private syncMyUserToAllGroups;
|
|
56
79
|
private syncMyUserToGroup;
|
|
80
|
+
/**
|
|
81
|
+
* Subscribe to `dataChanged` events for one table across the personal
|
|
82
|
+
* context and every loaded group context.
|
|
83
|
+
*
|
|
84
|
+
* @param table - Table instance or table name.
|
|
85
|
+
* @param handler - Invoked with the event plus the owning data context.
|
|
86
|
+
*/
|
|
57
87
|
subscribeToDataChangedAcrossAllGroups<T extends {
|
|
58
88
|
[key: string]: any;
|
|
59
89
|
}>(table: string | Table<T>, handler: (evt: ICrossGroupSubscriptionHandlerArgs<T>) => any): import("../events").ISubscriptionResult;
|
|
90
|
+
/**
|
|
91
|
+
* Dispose resources for the personal context and every loaded group context.
|
|
92
|
+
*
|
|
93
|
+
* The legacy method name is retained for callers that use this during
|
|
94
|
+
* Electron shutdown; disposal now also clears each context's event registry.
|
|
95
|
+
*/
|
|
96
|
+
disposeIsolatedRuntimes(): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Unsubscribe cross-group listeners and dispose isolated package runtimes.
|
|
99
|
+
*
|
|
100
|
+
* Runtime disposal is started but not awaited. Prefer
|
|
101
|
+
* {@link disposeIsolatedRuntimes} when the caller can wait (tests,
|
|
102
|
+
* Electron shutdown).
|
|
103
|
+
*/
|
|
60
104
|
dispose(): void;
|
|
61
105
|
}
|
|
62
106
|
/** Event payload supplied to subscriptions spanning every group data context. */
|
|
@@ -23,8 +23,11 @@ class UserContext {
|
|
|
23
23
|
loadingPromise;
|
|
24
24
|
personalUserSubscription;
|
|
25
25
|
ownUserSubscription;
|
|
26
|
+
groupsSubscription;
|
|
27
|
+
observableSubscriptions = [];
|
|
26
28
|
groupUserSyncs = new Map();
|
|
27
29
|
_currentlyActiveGroupIdPVar;
|
|
30
|
+
disposed = false;
|
|
28
31
|
constructor(userId, dataSourceFactory, ephemeral) {
|
|
29
32
|
this.userId = userId;
|
|
30
33
|
this.dataSourceFactory = dataSourceFactory;
|
|
@@ -43,6 +46,9 @@ class UserContext {
|
|
|
43
46
|
if (!this.ephemeral) {
|
|
44
47
|
await this.loadUserContextObservablesFromDB();
|
|
45
48
|
}
|
|
49
|
+
if (this.disposed) {
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
46
52
|
await this.loadGroupContexts();
|
|
47
53
|
return this;
|
|
48
54
|
}
|
|
@@ -64,12 +70,14 @@ class UserContext {
|
|
|
64
70
|
async loadGroupContexts() {
|
|
65
71
|
const groupsTable = (0, data_1.Groups)(this.userDataContext);
|
|
66
72
|
const groups = await groupsTable.list({ disabled: { $ne: true } });
|
|
73
|
+
if (this.disposed) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
67
76
|
this.groupIds(groups.map((g) => g.groupId));
|
|
68
|
-
groupsTable.dataChanged.subscribe((evt) => {
|
|
77
|
+
this.groupsSubscription = groupsTable.dataChanged.subscribe((evt) => {
|
|
69
78
|
const groupId = evt.dataObject.groupId;
|
|
70
79
|
if (evt.op === "delete" || evt.dataObject.disabled) {
|
|
71
|
-
this.
|
|
72
|
-
this.groupDataContexts.delete(groupId);
|
|
80
|
+
this.removeGroupDataContext(groupId);
|
|
73
81
|
}
|
|
74
82
|
else {
|
|
75
83
|
const dataContext = this.getDataContext(groupId);
|
|
@@ -91,22 +99,39 @@ class UserContext {
|
|
|
91
99
|
this.defaultDataContext(this.userDataContext);
|
|
92
100
|
}
|
|
93
101
|
// ensure when currentlyActiveGroupId changes, currentlyActiveGroup is updated and vice versa
|
|
94
|
-
this.currentlyActiveGroupId.subscribe(async (groupId) => {
|
|
102
|
+
this.observableSubscriptions.push(this.currentlyActiveGroupId.subscribe(async (groupId) => {
|
|
95
103
|
const dataContext = this.getDataContext(groupId || this.userId);
|
|
96
104
|
this.defaultDataContext(dataContext);
|
|
97
|
-
})
|
|
98
|
-
this.defaultDataContext.subscribe((group) => {
|
|
105
|
+
}), this.defaultDataContext.subscribe((group) => {
|
|
99
106
|
if (!group) {
|
|
100
107
|
this.defaultDataContext(this.userDataContext);
|
|
101
108
|
}
|
|
102
109
|
else {
|
|
103
110
|
this.currentlyActiveGroupId(group.dataContextId);
|
|
104
111
|
}
|
|
105
|
-
});
|
|
112
|
+
}));
|
|
106
113
|
if (!this.currentlyActiveGroupId()) {
|
|
107
114
|
this.currentlyActiveGroupId(this.defaultDataContext().dataContextId || this.userId);
|
|
108
115
|
}
|
|
109
116
|
}
|
|
117
|
+
removeGroupDataContext(groupId) {
|
|
118
|
+
this.groupIds(this.groupIds().filter((id) => id !== groupId));
|
|
119
|
+
const removedContext = this.groupDataContexts.get(groupId);
|
|
120
|
+
this.groupDataContexts.delete(groupId);
|
|
121
|
+
this.groupUserSyncs.delete(groupId);
|
|
122
|
+
if (this.defaultDataContext().dataContextId === groupId) {
|
|
123
|
+
this.defaultDataContext(this.userDataContext);
|
|
124
|
+
}
|
|
125
|
+
void removedContext?.dispose().catch((error) => {
|
|
126
|
+
console.error(`Error disposing removed group context ${groupId}:`, error);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Return the personal context, or the group context for `groupId`, creating
|
|
131
|
+
* the group context if this user context does not already hold it.
|
|
132
|
+
*
|
|
133
|
+
* @param groupId - Group id, or the current user id for the personal context.
|
|
134
|
+
*/
|
|
110
135
|
getDataContext(groupId) {
|
|
111
136
|
if (!groupId || groupId === this.userId) {
|
|
112
137
|
return this.userDataContext;
|
|
@@ -121,7 +146,11 @@ class UserContext {
|
|
|
121
146
|
const groupContext = new data_context_1.DataContext(this, groupId);
|
|
122
147
|
this.groupDataContexts.set(groupId, groupContext);
|
|
123
148
|
}
|
|
124
|
-
|
|
149
|
+
const dataContext = this.groupDataContexts.get(groupId);
|
|
150
|
+
if (!dataContext) {
|
|
151
|
+
throw new Error(`Failed to create data context for group ${groupId}`);
|
|
152
|
+
}
|
|
153
|
+
return dataContext;
|
|
125
154
|
}
|
|
126
155
|
/** Wait for any pending `currentlyActiveGroupId` pvar write to be flushed to the DB. */
|
|
127
156
|
async waitForActiveGroupPersistence() {
|
|
@@ -140,6 +169,9 @@ class UserContext {
|
|
|
140
169
|
const vars = await persistentVars.list({
|
|
141
170
|
name: { $in: ["thisDeviceId", "currentlyActiveGroupId", "reloadPackagesOnPageRefresh"] },
|
|
142
171
|
});
|
|
172
|
+
if (this.disposed) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
143
175
|
const varDbValues = vars.reduce((acc, curr) => {
|
|
144
176
|
acc[curr.name] = curr.value?.value;
|
|
145
177
|
return acc;
|
|
@@ -151,7 +183,7 @@ class UserContext {
|
|
|
151
183
|
userContext: this,
|
|
152
184
|
});
|
|
153
185
|
this.deviceId(deviceIdPVar());
|
|
154
|
-
(0, observable_1.linkObservables)(deviceIdPVar, this.deviceId);
|
|
186
|
+
this.observableSubscriptions.push((0, observable_1.linkObservables)(deviceIdPVar, this.deviceId));
|
|
155
187
|
const currentlyActiveGroupIdPVar = (0, data_1.deviceVar)("currentlyActiveGroupId", {
|
|
156
188
|
dbValue: varDbValues.currentlyActiveGroupId,
|
|
157
189
|
defaultValue: "",
|
|
@@ -159,24 +191,37 @@ class UserContext {
|
|
|
159
191
|
});
|
|
160
192
|
this._currentlyActiveGroupIdPVar = currentlyActiveGroupIdPVar;
|
|
161
193
|
this.currentlyActiveGroupId(currentlyActiveGroupIdPVar());
|
|
162
|
-
(0, observable_1.linkObservables)(currentlyActiveGroupIdPVar, this.currentlyActiveGroupId);
|
|
194
|
+
this.observableSubscriptions.push((0, observable_1.linkObservables)(currentlyActiveGroupIdPVar, this.currentlyActiveGroupId));
|
|
163
195
|
const reloadPackagesOnPageRefreshPVar = (0, data_1.deviceVar)("reloadPackagesOnPageRefresh", {
|
|
164
196
|
defaultValue: false,
|
|
165
197
|
dbValue: varDbValues.reloadPackagesOnPageRefresh,
|
|
166
198
|
userContext: this,
|
|
167
199
|
});
|
|
168
200
|
this.reloadPackagesOnPageRefresh(reloadPackagesOnPageRefreshPVar());
|
|
169
|
-
(0, observable_1.linkObservables)(reloadPackagesOnPageRefreshPVar, this.reloadPackagesOnPageRefresh);
|
|
201
|
+
this.observableSubscriptions.push((0, observable_1.linkObservables)(reloadPackagesOnPageRefreshPVar, this.reloadPackagesOnPageRefresh));
|
|
170
202
|
await Promise.all([
|
|
171
203
|
deviceIdPVar.loadingPromise,
|
|
172
204
|
currentlyActiveGroupIdPVar.loadingPromise,
|
|
173
205
|
reloadPackagesOnPageRefreshPVar.loadingPromise,
|
|
174
206
|
]);
|
|
175
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Load the signed-in user's `Users` row from the personal data context.
|
|
210
|
+
*
|
|
211
|
+
* @returns The cached personal user record, or `undefined` if it is missing.
|
|
212
|
+
*/
|
|
176
213
|
async getMe() {
|
|
177
214
|
const dbMe = await (0, data_1.Users)(this.userDataContext).get(this.userId, { useCache: true });
|
|
178
215
|
return dbMe;
|
|
179
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Sign and persist this user's identity, then copy it into every loaded
|
|
219
|
+
* group context and copy group records back into the personal context.
|
|
220
|
+
*
|
|
221
|
+
* @param userId - Signed-in user id.
|
|
222
|
+
* @param keys - Current public/private keypair used to sign the user row.
|
|
223
|
+
* @param me - Optional preloaded personal user row.
|
|
224
|
+
*/
|
|
180
225
|
async syncUserAndGroupObjects(userId, keys, me) {
|
|
181
226
|
me ??= await this.getMe();
|
|
182
227
|
if (!me) {
|
|
@@ -274,12 +319,21 @@ class UserContext {
|
|
|
274
319
|
}
|
|
275
320
|
}
|
|
276
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Subscribe to `dataChanged` events for one table across the personal
|
|
324
|
+
* context and every loaded group context.
|
|
325
|
+
*
|
|
326
|
+
* @param table - Table instance or table name.
|
|
327
|
+
* @param handler - Invoked with the event plus the owning data context.
|
|
328
|
+
*/
|
|
277
329
|
subscribeToDataChangedAcrossAllGroups(table, handler) {
|
|
278
330
|
const tableName = typeof table === "string" ? table : table.tableName;
|
|
279
331
|
const tableEventPrefix = `${tableName}_DataChanged_`;
|
|
280
332
|
const unsubscribePrefix = (0, events_1.subscribePrefix)(tableEventPrefix);
|
|
281
333
|
const subscription = (0, events_1.subscribe)((evt) => evt.name.startsWith(tableEventPrefix), async (evt) => {
|
|
282
|
-
const dataContextId = evt.name.endsWith("_")
|
|
334
|
+
const dataContextId = evt.name.endsWith("_")
|
|
335
|
+
? this.userId
|
|
336
|
+
: (evt.name.split("_").pop() ?? this.userId);
|
|
283
337
|
const dataContext = this.getDataContext(dataContextId);
|
|
284
338
|
handler({
|
|
285
339
|
name: evt.name,
|
|
@@ -295,9 +349,34 @@ class UserContext {
|
|
|
295
349
|
};
|
|
296
350
|
return subscription;
|
|
297
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* Dispose resources for the personal context and every loaded group context.
|
|
354
|
+
*
|
|
355
|
+
* The legacy method name is retained for callers that use this during
|
|
356
|
+
* Electron shutdown; disposal now also clears each context's event registry.
|
|
357
|
+
*/
|
|
358
|
+
async disposeIsolatedRuntimes() {
|
|
359
|
+
await Promise.all([
|
|
360
|
+
this.userDataContext.dispose(),
|
|
361
|
+
...[...this.groupDataContexts.values()].map((context) => context.dispose()),
|
|
362
|
+
]);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Unsubscribe cross-group listeners and dispose isolated package runtimes.
|
|
366
|
+
*
|
|
367
|
+
* Runtime disposal is started but not awaited. Prefer
|
|
368
|
+
* {@link disposeIsolatedRuntimes} when the caller can wait (tests,
|
|
369
|
+
* Electron shutdown).
|
|
370
|
+
*/
|
|
298
371
|
dispose() {
|
|
372
|
+
this.disposed = true;
|
|
373
|
+
this.groupsSubscription?.unsubscribe();
|
|
299
374
|
this.personalUserSubscription?.unsubscribe();
|
|
300
375
|
this.ownUserSubscription?.unsubscribe();
|
|
376
|
+
for (const subscription of this.observableSubscriptions.splice(0)) {
|
|
377
|
+
subscription.dispose();
|
|
378
|
+
}
|
|
379
|
+
void this.disposeIsolatedRuntimes();
|
|
301
380
|
}
|
|
302
381
|
}
|
|
303
382
|
exports.UserContext = UserContext;
|
|
@@ -64,6 +64,32 @@ describe("contract provider router", () => {
|
|
|
64
64
|
expect(resolveEndpoint).not.toHaveBeenCalled();
|
|
65
65
|
router.dispose();
|
|
66
66
|
});
|
|
67
|
+
it("keys endpoints by provider and invalidates one provider without trusting call payload", async () => {
|
|
68
|
+
const [consumerTransport, providerTransport] = (0, index_1.inProcessTransportPair)();
|
|
69
|
+
let activeProvider = "provider-a";
|
|
70
|
+
const created = [];
|
|
71
|
+
const router = (0, index_1.createContractProviderRouter)(providerTransport, {
|
|
72
|
+
authorizeCall: () => ({ dataContextId: "personal-context" }),
|
|
73
|
+
resolveProviderPackageId: () => activeProvider,
|
|
74
|
+
resolveEndpoint: (request) => {
|
|
75
|
+
const providerPackageId = request.providerPackageId ?? "missing";
|
|
76
|
+
const endpoint = makeEndpoint(providerPackageId);
|
|
77
|
+
created.push({ providerPackageId, endpoint });
|
|
78
|
+
return endpoint;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
await expect(consumerTransport.emit(index_1.CONTRACT_REQUEST_CHANNEL, makeCall())).resolves.toBe("provider-a:list");
|
|
82
|
+
activeProvider = "provider-b";
|
|
83
|
+
await expect(consumerTransport.emit(index_1.CONTRACT_REQUEST_CHANNEL, makeCall())).resolves.toBe("provider-b:list");
|
|
84
|
+
expect(created).toHaveLength(2);
|
|
85
|
+
router.invalidateProvider("provider-a");
|
|
86
|
+
expect(created[0].endpoint.dispose).toHaveBeenCalledTimes(1);
|
|
87
|
+
expect(created[1].endpoint.dispose).not.toHaveBeenCalled();
|
|
88
|
+
activeProvider = "provider-a";
|
|
89
|
+
await expect(consumerTransport.emit(index_1.CONTRACT_REQUEST_CHANNEL, makeCall())).resolves.toBe("provider-a:list");
|
|
90
|
+
expect(created).toHaveLength(3);
|
|
91
|
+
router.dispose();
|
|
92
|
+
});
|
|
67
93
|
it("ignores peer-supplied context in favor of trusted host context", async () => {
|
|
68
94
|
const [consumerTransport, providerTransport] = (0, index_1.inProcessTransportPair)();
|
|
69
95
|
const trustedContext = {
|
|
@@ -259,6 +259,22 @@ describe("local user-context contract routing", () => {
|
|
|
259
259
|
expect(setup.getDataContext).not.toHaveBeenCalledWith(unknownId);
|
|
260
260
|
router.dispose();
|
|
261
261
|
});
|
|
262
|
+
it("prefers an isolated package endpoint over an in-process resolver", async () => {
|
|
263
|
+
const setup = setupLocalRouter();
|
|
264
|
+
const isolatedHandle = jest.fn(async () => "from-isolated");
|
|
265
|
+
setup.userContext.userDataContext.packageLoader = {
|
|
266
|
+
resolveIsolatedEndpoint: jest.fn(async () => ({
|
|
267
|
+
handleCall: isolatedHandle,
|
|
268
|
+
dispose() { },
|
|
269
|
+
})),
|
|
270
|
+
};
|
|
271
|
+
const router = (0, index_1.createLocalUserContextContractProviderRouter)(setup.providerTransport, {
|
|
272
|
+
getUserContext: async () => setup.userContext,
|
|
273
|
+
});
|
|
274
|
+
await expect(setup.consumerTransport.emit(index_1.CONTRACT_REQUEST_CHANNEL, makeToolCall(setup.contractId))).resolves.toBe("from-isolated");
|
|
275
|
+
expect(isolatedHandle).toHaveBeenCalled();
|
|
276
|
+
router.dispose();
|
|
277
|
+
});
|
|
262
278
|
it("enforces an optional contract allowlist before resolving a provider", async () => {
|
|
263
279
|
const setup = setupLocalRouter();
|
|
264
280
|
const router = (0, index_1.createLocalUserContextContractProviderRouter)(setup.providerTransport, {
|
|
@@ -305,3 +321,31 @@ describe("local user-context contract routing", () => {
|
|
|
305
321
|
router.dispose();
|
|
306
322
|
});
|
|
307
323
|
});
|
|
324
|
+
describe("provider-scoped contract resolvers", () => {
|
|
325
|
+
it("prefers the selected provider and preserves the unscoped fallback", () => {
|
|
326
|
+
const contractId = (0, utils_1.newid)();
|
|
327
|
+
const providerA = (0, utils_1.newid)();
|
|
328
|
+
const providerB = (0, utils_1.newid)();
|
|
329
|
+
const unscoped = jest.fn(() => ({ contractId, version: 1 }));
|
|
330
|
+
const scopedA = jest.fn(() => ({ contractId, version: 1 }));
|
|
331
|
+
(0, index_1.registerContractResolver)(contractId, 1, unscoped);
|
|
332
|
+
(0, index_1.registerContractResolver)(contractId, 1, scopedA, {
|
|
333
|
+
providerPackageId: providerA,
|
|
334
|
+
});
|
|
335
|
+
expect((0, index_1.getContractResolver)(contractId, 1, providerA)).toBe(scopedA);
|
|
336
|
+
expect((0, index_1.getContractResolver)(contractId, 1, providerB)).toBe(unscoped);
|
|
337
|
+
expect((0, index_1.getContractResolver)(contractId, 1)).toBe(unscoped);
|
|
338
|
+
});
|
|
339
|
+
it("supports legacy unscoped lookup only when a scoped provider is unambiguous", () => {
|
|
340
|
+
const contractId = (0, utils_1.newid)();
|
|
341
|
+
const providerA = (0, utils_1.newid)();
|
|
342
|
+
const providerB = (0, utils_1.newid)();
|
|
343
|
+
const scopedA = jest.fn(() => ({ contractId, version: 1 }));
|
|
344
|
+
const scopedB = jest.fn(() => ({ contractId, version: 1 }));
|
|
345
|
+
(0, index_1.registerContractResolver)(contractId, 1, scopedA, { providerPackageId: providerA });
|
|
346
|
+
expect((0, index_1.getContractResolver)(contractId, 1)).toBe(scopedA);
|
|
347
|
+
(0, index_1.registerContractResolver)(contractId, 1, scopedB, { providerPackageId: providerB });
|
|
348
|
+
expect((0, index_1.getContractResolver)(contractId, 1)).toBeUndefined();
|
|
349
|
+
expect((0, index_1.getContractResolver)(contractId, 1, providerB)).toBe(scopedB);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
@@ -64,3 +64,107 @@ describe("ContractRegistry.selectProvider", () => {
|
|
|
64
64
|
expect(empty.selectProvider({ contractId, version: 1 })).toBeUndefined();
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
|
+
describe("ContractRegistry.registerBatch", () => {
|
|
68
|
+
it("registers same-batch alsoImplements targets atomically", () => {
|
|
69
|
+
const registry = new registry_1.ContractRegistry();
|
|
70
|
+
const providerPackageId = (0, utils_1.newid)();
|
|
71
|
+
const targetContractId = (0, utils_1.newid)();
|
|
72
|
+
const implementationContractId = (0, utils_1.newid)();
|
|
73
|
+
const result = registry.registerBatch(providerPackageId, [
|
|
74
|
+
{ definition: makeDefinition(targetContractId, 1, "Target") },
|
|
75
|
+
{
|
|
76
|
+
definition: makeDefinition(implementationContractId, 2, "Implementation"),
|
|
77
|
+
alsoImplements: [{ contractId: targetContractId, version: 1 }],
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
80
|
+
expect(result.valid).toBe(true);
|
|
81
|
+
expect(registry.getProviderPackageId(targetContractId, 1)).toBe(providerPackageId);
|
|
82
|
+
expect(registry.getProviderPackageId(implementationContractId, 2)).toBe(providerPackageId);
|
|
83
|
+
});
|
|
84
|
+
it("registers alsoImplements as a routable provider alias", () => {
|
|
85
|
+
const registry = new registry_1.ContractRegistry();
|
|
86
|
+
const originalProvider = (0, utils_1.newid)();
|
|
87
|
+
const compatibleProvider = (0, utils_1.newid)();
|
|
88
|
+
const contractId = (0, utils_1.newid)();
|
|
89
|
+
registry.register(originalProvider, makeDefinition(contractId, 1, "Version 1"));
|
|
90
|
+
const result = registry.registerBatch(compatibleProvider, [
|
|
91
|
+
{
|
|
92
|
+
definition: makeDefinition(contractId, 2, "Version 2"),
|
|
93
|
+
alsoImplements: [{ contractId, version: 1 }],
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
expect(result.valid).toBe(true);
|
|
97
|
+
expect(registry.getRegisteredProviderIds(contractId, 1)).toEqual([
|
|
98
|
+
originalProvider,
|
|
99
|
+
compatibleProvider,
|
|
100
|
+
]);
|
|
101
|
+
expect(registry.getProviderImplementation(contractId, 1, compatibleProvider)).toEqual({
|
|
102
|
+
contractId,
|
|
103
|
+
version: 2,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
it("leaves registry state unchanged when any batch entry fails", () => {
|
|
107
|
+
const registry = new registry_1.ContractRegistry();
|
|
108
|
+
const existingProvider = (0, utils_1.newid)();
|
|
109
|
+
const rejectedProvider = (0, utils_1.newid)();
|
|
110
|
+
const existingContractId = (0, utils_1.newid)();
|
|
111
|
+
const newContractId = (0, utils_1.newid)();
|
|
112
|
+
const frozen = { ...makeDefinition(existingContractId, 1, "Frozen"), devTag: undefined };
|
|
113
|
+
expect(registry.register(existingProvider, frozen).valid).toBe(true);
|
|
114
|
+
const result = registry.registerBatch(rejectedProvider, [
|
|
115
|
+
{ definition: makeDefinition(newContractId, 1, "Would Be Partial") },
|
|
116
|
+
{ definition: makeDefinition(existingContractId, 1, "Frozen Changed") },
|
|
117
|
+
]);
|
|
118
|
+
expect(result.valid).toBe(false);
|
|
119
|
+
expect(registry.getDefinition(newContractId, 1)).toBeUndefined();
|
|
120
|
+
expect(registry.getRegisteredProviderIds(newContractId, 1)).toEqual([]);
|
|
121
|
+
expect(registry.getProviderPackageId(existingContractId, 1)).toBe(existingProvider);
|
|
122
|
+
expect(registry.getRegisteredProviderIds(existingContractId, 1)).toEqual([existingProvider]);
|
|
123
|
+
});
|
|
124
|
+
it("preserves the existing active provider when another package registers a batch", () => {
|
|
125
|
+
const registry = new registry_1.ContractRegistry();
|
|
126
|
+
const contractId = (0, utils_1.newid)();
|
|
127
|
+
const activeProvider = (0, utils_1.newid)();
|
|
128
|
+
const additionalProvider = (0, utils_1.newid)();
|
|
129
|
+
registry.register(activeProvider, makeDefinition(contractId, 1, "Active"));
|
|
130
|
+
expect(registry.registerBatch(additionalProvider, [
|
|
131
|
+
{ definition: makeDefinition(contractId, 1, "Additional") },
|
|
132
|
+
]).valid).toBe(true);
|
|
133
|
+
expect(registry.getProviderPackageId(contractId, 1)).toBe(activeProvider);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe("ContractRegistry.replaceBatch", () => {
|
|
137
|
+
it("replaces the provider's complete contract set and preserves its active selections", () => {
|
|
138
|
+
const registry = new registry_1.ContractRegistry();
|
|
139
|
+
const providerPackageId = (0, utils_1.newid)();
|
|
140
|
+
const retainedContractId = (0, utils_1.newid)();
|
|
141
|
+
const removedContractId = (0, utils_1.newid)();
|
|
142
|
+
const addedContractId = (0, utils_1.newid)();
|
|
143
|
+
registry.registerBatch(providerPackageId, [
|
|
144
|
+
{ definition: makeDefinition(retainedContractId, 1, "Retained") },
|
|
145
|
+
{ definition: makeDefinition(removedContractId, 1, "Removed") },
|
|
146
|
+
]);
|
|
147
|
+
const result = registry.replaceBatch(providerPackageId, [
|
|
148
|
+
{ definition: makeDefinition(retainedContractId, 1, "Retained replacement") },
|
|
149
|
+
{ definition: makeDefinition(addedContractId, 1, "Added") },
|
|
150
|
+
]);
|
|
151
|
+
expect(result.valid).toBe(true);
|
|
152
|
+
expect(registry.getProviderPackageId(retainedContractId, 1)).toBe(providerPackageId);
|
|
153
|
+
expect(registry.getRegisteredProviderIds(removedContractId, 1)).toEqual([]);
|
|
154
|
+
expect(registry.getProviderPackageId(addedContractId, 1)).toBe(providerPackageId);
|
|
155
|
+
});
|
|
156
|
+
it("leaves the previous provider set untouched when replacement validation fails", () => {
|
|
157
|
+
const registry = new registry_1.ContractRegistry();
|
|
158
|
+
const providerPackageId = (0, utils_1.newid)();
|
|
159
|
+
const existingContractId = (0, utils_1.newid)();
|
|
160
|
+
const rejectedContractId = (0, utils_1.newid)();
|
|
161
|
+
registry.register(providerPackageId, makeDefinition(existingContractId, 1, "Existing"));
|
|
162
|
+
const result = registry.replaceBatch(providerPackageId, [
|
|
163
|
+
{ definition: makeDefinition(rejectedContractId, 1, "Rejected") },
|
|
164
|
+
{ definition: makeDefinition(rejectedContractId, 1, "Duplicate") },
|
|
165
|
+
]);
|
|
166
|
+
expect(result.valid).toBe(false);
|
|
167
|
+
expect(registry.getProviderPackageId(existingContractId, 1)).toBe(providerPackageId);
|
|
168
|
+
expect(registry.getRegisteredProviderIds(rejectedContractId, 1)).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
});
|