@serfab/cadre-core 0.0.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/README.md +252 -0
- package/dist/arachnode-stub.d.ts +60 -0
- package/dist/arachnode-stub.js +120 -0
- package/dist/arachnode-stub.js.map +1 -0
- package/dist/cadre-node.d.ts +252 -0
- package/dist/cadre-node.js +753 -0
- package/dist/cadre-node.js.map +1 -0
- package/dist/control-database.d.ts +62 -0
- package/dist/control-database.js +295 -0
- package/dist/control-database.js.map +1 -0
- package/dist/enrollment.d.ts +76 -0
- package/dist/enrollment.js +126 -0
- package/dist/enrollment.js.map +1 -0
- package/dist/hibernation-manager.d.ts +68 -0
- package/dist/hibernation-manager.js +212 -0
- package/dist/hibernation-manager.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/seed-bootstrap.d.ts +164 -0
- package/dist/seed-bootstrap.js +554 -0
- package/dist/seed-bootstrap.js.map +1 -0
- package/dist/strand-database.d.ts +39 -0
- package/dist/strand-database.js +108 -0
- package/dist/strand-database.js.map +1 -0
- package/dist/strand-formation-manager.d.ts +87 -0
- package/dist/strand-formation-manager.js +168 -0
- package/dist/strand-formation-manager.js.map +1 -0
- package/dist/strand-instance-manager.d.ts +60 -0
- package/dist/strand-instance-manager.js +209 -0
- package/dist/strand-instance-manager.js.map +1 -0
- package/dist/strand-solicitation.d.ts +145 -0
- package/dist/strand-solicitation.js +180 -0
- package/dist/strand-solicitation.js.map +1 -0
- package/dist/strand-watcher.d.ts +70 -0
- package/dist/strand-watcher.js +136 -0
- package/dist/strand-watcher.js.map +1 -0
- package/dist/types.d.ts +484 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/package.json +67 -0
- package/src/arachnode-stub.ts +160 -0
- package/src/cadre-node.ts +923 -0
- package/src/control-database.ts +376 -0
- package/src/enrollment.ts +199 -0
- package/src/hibernation-manager.ts +268 -0
- package/src/index.ts +71 -0
- package/src/seed-bootstrap.ts +753 -0
- package/src/strand-database.ts +168 -0
- package/src/strand-formation-manager.ts +277 -0
- package/src/strand-instance-manager.ts +281 -0
- package/src/strand-solicitation.ts +318 -0
- package/src/strand-watcher.ts +197 -0
- package/src/types.ts +553 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { Libp2p } from '@libp2p/interface';
|
|
2
|
+
import type { OpenInvitation, FormStrandResult, ValidateFormationResult, StrandFormationDisclosure } from './types.js';
|
|
3
|
+
import { type StrandFormationManagerConfig } from './strand-formation-manager.js';
|
|
4
|
+
/**
|
|
5
|
+
* Interface for validating formation disclosures
|
|
6
|
+
*/
|
|
7
|
+
export interface DisclosureValidator {
|
|
8
|
+
/**
|
|
9
|
+
* Validate the disclosure provided by an initiator
|
|
10
|
+
* @param token The invitation token
|
|
11
|
+
* @param disclosure The disclosure object from the initiator
|
|
12
|
+
* @returns Whether the disclosure is acceptable
|
|
13
|
+
*/
|
|
14
|
+
validateDisclosure(token: string, disclosure: StrandFormationDisclosure): Promise<boolean>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Interface for recording formation usage
|
|
18
|
+
*/
|
|
19
|
+
export interface FormationUsageRecorder {
|
|
20
|
+
/**
|
|
21
|
+
* Record that a formation invite was used
|
|
22
|
+
*/
|
|
23
|
+
recordUsage(token: string, initiatorKey: string, strandId: string): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Check if a token has already been used (for single-use invites)
|
|
26
|
+
*/
|
|
27
|
+
isTokenUsed(token: string): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Check if a token is valid and not expired
|
|
30
|
+
*/
|
|
31
|
+
isTokenValid(token: string): Promise<{
|
|
32
|
+
valid: boolean;
|
|
33
|
+
invitation?: OpenInvitation;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Interface for strand provisioning during formation
|
|
38
|
+
*/
|
|
39
|
+
export interface StrandProvisioner {
|
|
40
|
+
/**
|
|
41
|
+
* Provision a new strand after formation is validated
|
|
42
|
+
*/
|
|
43
|
+
provisionStrand(sAppId: string, initiatorKey: string, responderKey: string): Promise<{
|
|
44
|
+
strandId: string;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Interface for signing formation approvals
|
|
49
|
+
*/
|
|
50
|
+
export interface FormationSigner {
|
|
51
|
+
/**
|
|
52
|
+
* Sign a formation approval
|
|
53
|
+
*/
|
|
54
|
+
signFormation(token: string, disclosure: StrandFormationDisclosure): Promise<{
|
|
55
|
+
validationKey: string;
|
|
56
|
+
validationSignature: string;
|
|
57
|
+
}>;
|
|
58
|
+
}
|
|
59
|
+
export interface StrandSolicitationServiceOptions {
|
|
60
|
+
disclosureValidator?: DisclosureValidator;
|
|
61
|
+
formationUsageRecorder?: FormationUsageRecorder;
|
|
62
|
+
strandProvisioner?: StrandProvisioner;
|
|
63
|
+
formationSigner?: FormationSigner;
|
|
64
|
+
/** Party ID for this node (used in protocol messages) */
|
|
65
|
+
partyId?: string;
|
|
66
|
+
/** Cadre peer addresses for this node */
|
|
67
|
+
cadrePeerAddrs?: string[];
|
|
68
|
+
/** Configuration for the formation manager */
|
|
69
|
+
formationConfig?: StrandFormationManagerConfig;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Strand Solicitation API for forming strands via open invitations.
|
|
73
|
+
*
|
|
74
|
+
* This service handles the high-level API defined in api.md:
|
|
75
|
+
* - formStrand(invitation, disclosure, node) - called by initiator
|
|
76
|
+
* - validateStrandFormation(token, disclosure) - called by responder
|
|
77
|
+
*
|
|
78
|
+
* When a libp2p node is provided, the underlying protocol is handled by
|
|
79
|
+
* strand-proto's SessionManager via StrandFormationManager.
|
|
80
|
+
*/
|
|
81
|
+
export declare class StrandSolicitationService {
|
|
82
|
+
private readonly disclosureValidator?;
|
|
83
|
+
private readonly formationUsageRecorder?;
|
|
84
|
+
private readonly strandProvisioner?;
|
|
85
|
+
private readonly formationSigner?;
|
|
86
|
+
private readonly partyId;
|
|
87
|
+
private readonly cadrePeerAddrs;
|
|
88
|
+
private formationManager?;
|
|
89
|
+
private readonly formationConfig?;
|
|
90
|
+
constructor(options?: StrandSolicitationServiceOptions);
|
|
91
|
+
/**
|
|
92
|
+
* Get or create the StrandFormationManager.
|
|
93
|
+
* Lazily initialized to allow configuration after construction.
|
|
94
|
+
*/
|
|
95
|
+
private getFormationManager;
|
|
96
|
+
/**
|
|
97
|
+
* Register as a responder on a libp2p node.
|
|
98
|
+
* This enables the node to handle incoming strand formation requests.
|
|
99
|
+
*/
|
|
100
|
+
registerResponder(node: Libp2p): void;
|
|
101
|
+
/**
|
|
102
|
+
* Unregister as a responder from a libp2p node.
|
|
103
|
+
*/
|
|
104
|
+
unregisterResponder(node: Libp2p): void;
|
|
105
|
+
/**
|
|
106
|
+
* Form a strand with a responder via an open invitation.
|
|
107
|
+
*
|
|
108
|
+
* Called by the initiator (the party who received an out-of-band invitation).
|
|
109
|
+
* This generates a member key, contacts the responder's cadre, and negotiates
|
|
110
|
+
* strand formation.
|
|
111
|
+
*
|
|
112
|
+
* @param invitation The open invitation (or just token for legacy API)
|
|
113
|
+
* @param disclosure Identity/context information to share with the responder
|
|
114
|
+
* @param node Optional libp2p node for real protocol handling
|
|
115
|
+
* @returns The member key and strand info if successful
|
|
116
|
+
*/
|
|
117
|
+
formStrand(invitation: OpenInvitation | string, disclosure: StrandFormationDisclosure, node?: Libp2p): Promise<FormStrandResult>;
|
|
118
|
+
/**
|
|
119
|
+
* Validate a strand formation request.
|
|
120
|
+
*
|
|
121
|
+
* Called by the responder (the party who created the open invitation) when
|
|
122
|
+
* an initiator contacts them to form a strand. Validates the disclosure
|
|
123
|
+
* and returns a signed approval.
|
|
124
|
+
*
|
|
125
|
+
* @param token The invitation token being used
|
|
126
|
+
* @param disclosure The disclosure from the initiator
|
|
127
|
+
* @returns Validation key and signature if approved
|
|
128
|
+
* @throws If validation fails or token is invalid
|
|
129
|
+
*/
|
|
130
|
+
validateStrandFormation(token: string, disclosure: StrandFormationDisclosure): Promise<ValidateFormationResult>;
|
|
131
|
+
/**
|
|
132
|
+
* Create an open invitation for others to form strands with this party.
|
|
133
|
+
*
|
|
134
|
+
* @param sAppId The sApp to use for formed strands
|
|
135
|
+
* @param expirationMs How long the invitation is valid (ms from now)
|
|
136
|
+
* @param bootstrap Bootstrap addresses for contacting this party's cadre
|
|
137
|
+
* @returns The open invitation to share out-of-band
|
|
138
|
+
*/
|
|
139
|
+
createOpenInvitation(sAppId: string, expirationMs: number, bootstrap: string[]): Promise<OpenInvitation>;
|
|
140
|
+
/**
|
|
141
|
+
* Record that a formation was completed successfully.
|
|
142
|
+
* Called after strand provisioning to track usage.
|
|
143
|
+
*/
|
|
144
|
+
recordFormationComplete(token: string, initiatorKey: string, strandId: string): Promise<void>;
|
|
145
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import { toString as uint8ArrayToString } from 'uint8arrays';
|
|
3
|
+
import { generateKeyPair, privateKeyToProtobuf } from '@libp2p/crypto/keys';
|
|
4
|
+
import { peerIdFromPrivateKey } from '@libp2p/peer-id';
|
|
5
|
+
import { StrandFormationManager } from './strand-formation-manager.js';
|
|
6
|
+
const log = debug('sereus:cadre:solicitation');
|
|
7
|
+
/**
|
|
8
|
+
* Strand Solicitation API for forming strands via open invitations.
|
|
9
|
+
*
|
|
10
|
+
* This service handles the high-level API defined in api.md:
|
|
11
|
+
* - formStrand(invitation, disclosure, node) - called by initiator
|
|
12
|
+
* - validateStrandFormation(token, disclosure) - called by responder
|
|
13
|
+
*
|
|
14
|
+
* When a libp2p node is provided, the underlying protocol is handled by
|
|
15
|
+
* strand-proto's SessionManager via StrandFormationManager.
|
|
16
|
+
*/
|
|
17
|
+
export class StrandSolicitationService {
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.disclosureValidator = options?.disclosureValidator;
|
|
20
|
+
this.formationUsageRecorder = options?.formationUsageRecorder;
|
|
21
|
+
this.strandProvisioner = options?.strandProvisioner;
|
|
22
|
+
this.formationSigner = options?.formationSigner;
|
|
23
|
+
this.partyId = options?.partyId ?? `party-${Date.now()}`;
|
|
24
|
+
this.cadrePeerAddrs = options?.cadrePeerAddrs ?? [];
|
|
25
|
+
this.formationConfig = options?.formationConfig;
|
|
26
|
+
log('StrandSolicitationService created for party: %s', this.partyId);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get or create the StrandFormationManager.
|
|
30
|
+
* Lazily initialized to allow configuration after construction.
|
|
31
|
+
*/
|
|
32
|
+
getFormationManager() {
|
|
33
|
+
if (!this.formationManager) {
|
|
34
|
+
this.formationManager = new StrandFormationManager({
|
|
35
|
+
disclosureValidator: this.disclosureValidator,
|
|
36
|
+
formationUsageRecorder: this.formationUsageRecorder,
|
|
37
|
+
strandProvisioner: this.strandProvisioner,
|
|
38
|
+
partyId: this.partyId,
|
|
39
|
+
cadrePeerAddrs: this.cadrePeerAddrs,
|
|
40
|
+
config: this.formationConfig
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return this.formationManager;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Register as a responder on a libp2p node.
|
|
47
|
+
* This enables the node to handle incoming strand formation requests.
|
|
48
|
+
*/
|
|
49
|
+
registerResponder(node) {
|
|
50
|
+
this.getFormationManager().registerResponder(node);
|
|
51
|
+
log('Registered as responder');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Unregister as a responder from a libp2p node.
|
|
55
|
+
*/
|
|
56
|
+
unregisterResponder(node) {
|
|
57
|
+
this.getFormationManager().unregisterResponder(node);
|
|
58
|
+
log('Unregistered as responder');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Form a strand with a responder via an open invitation.
|
|
62
|
+
*
|
|
63
|
+
* Called by the initiator (the party who received an out-of-band invitation).
|
|
64
|
+
* This generates a member key, contacts the responder's cadre, and negotiates
|
|
65
|
+
* strand formation.
|
|
66
|
+
*
|
|
67
|
+
* @param invitation The open invitation (or just token for legacy API)
|
|
68
|
+
* @param disclosure Identity/context information to share with the responder
|
|
69
|
+
* @param node Optional libp2p node for real protocol handling
|
|
70
|
+
* @returns The member key and strand info if successful
|
|
71
|
+
*/
|
|
72
|
+
async formStrand(invitation, disclosure, node) {
|
|
73
|
+
// Handle legacy API where just token was passed
|
|
74
|
+
const token = typeof invitation === 'string' ? invitation : invitation.token;
|
|
75
|
+
log('Forming strand with token: %s', token);
|
|
76
|
+
// Generate a new keypair for this strand membership
|
|
77
|
+
const privateKey = await generateKeyPair('Ed25519');
|
|
78
|
+
const peerId = peerIdFromPrivateKey(privateKey);
|
|
79
|
+
const privateKeyBytes = privateKeyToProtobuf(privateKey);
|
|
80
|
+
const memberKey = peerId.toString();
|
|
81
|
+
const invitePrivateKey = uint8ArrayToString(privateKeyBytes, 'base64');
|
|
82
|
+
log('Generated member key: %s', memberKey);
|
|
83
|
+
// If we have a full invitation and a node, use the real protocol
|
|
84
|
+
if (typeof invitation !== 'string' && node) {
|
|
85
|
+
log('Using strand-proto for real protocol handling');
|
|
86
|
+
const result = await this.getFormationManager().formStrand(invitation, { ...disclosure, partyId: memberKey }, node);
|
|
87
|
+
return {
|
|
88
|
+
memberKey,
|
|
89
|
+
invitePrivateKey,
|
|
90
|
+
strandId: result.strandId
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Fallback: placeholder strandId (for testing without network)
|
|
94
|
+
log('No node provided, using placeholder strandId');
|
|
95
|
+
const strandId = `strand-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
96
|
+
return {
|
|
97
|
+
memberKey,
|
|
98
|
+
invitePrivateKey,
|
|
99
|
+
strandId
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Validate a strand formation request.
|
|
104
|
+
*
|
|
105
|
+
* Called by the responder (the party who created the open invitation) when
|
|
106
|
+
* an initiator contacts them to form a strand. Validates the disclosure
|
|
107
|
+
* and returns a signed approval.
|
|
108
|
+
*
|
|
109
|
+
* @param token The invitation token being used
|
|
110
|
+
* @param disclosure The disclosure from the initiator
|
|
111
|
+
* @returns Validation key and signature if approved
|
|
112
|
+
* @throws If validation fails or token is invalid
|
|
113
|
+
*/
|
|
114
|
+
async validateStrandFormation(token, disclosure) {
|
|
115
|
+
log('Validating strand formation for token: %s', token);
|
|
116
|
+
// Check token validity
|
|
117
|
+
if (this.formationUsageRecorder) {
|
|
118
|
+
const tokenCheck = await this.formationUsageRecorder.isTokenValid(token);
|
|
119
|
+
if (!tokenCheck.valid) {
|
|
120
|
+
log('Token invalid or expired: %s', token);
|
|
121
|
+
throw new Error('Invalid or expired token');
|
|
122
|
+
}
|
|
123
|
+
// Check if already used (for single-use tokens)
|
|
124
|
+
const isUsed = await this.formationUsageRecorder.isTokenUsed(token);
|
|
125
|
+
if (isUsed) {
|
|
126
|
+
log('Token already used: %s', token);
|
|
127
|
+
throw new Error('Token has already been used');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Validate the disclosure
|
|
131
|
+
if (this.disclosureValidator) {
|
|
132
|
+
const isValid = await this.disclosureValidator.validateDisclosure(token, disclosure);
|
|
133
|
+
if (!isValid) {
|
|
134
|
+
log('Disclosure validation failed for token: %s', token);
|
|
135
|
+
throw new Error('Disclosure validation failed');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Sign the formation approval
|
|
139
|
+
if (!this.formationSigner) {
|
|
140
|
+
throw new Error('FormationSigner not configured');
|
|
141
|
+
}
|
|
142
|
+
const { validationKey, validationSignature } = await this.formationSigner.signFormation(token, disclosure);
|
|
143
|
+
log('Formation validated, key: %s', validationKey);
|
|
144
|
+
return {
|
|
145
|
+
validationKey,
|
|
146
|
+
validationSignature
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Create an open invitation for others to form strands with this party.
|
|
151
|
+
*
|
|
152
|
+
* @param sAppId The sApp to use for formed strands
|
|
153
|
+
* @param expirationMs How long the invitation is valid (ms from now)
|
|
154
|
+
* @param bootstrap Bootstrap addresses for contacting this party's cadre
|
|
155
|
+
* @returns The open invitation to share out-of-band
|
|
156
|
+
*/
|
|
157
|
+
async createOpenInvitation(sAppId, expirationMs, bootstrap) {
|
|
158
|
+
// Generate a unique token
|
|
159
|
+
const token = `invite-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
160
|
+
const expiration = new Date(Date.now() + expirationMs);
|
|
161
|
+
log('Created open invitation: %s (expires %s)', token, expiration.toISOString());
|
|
162
|
+
return {
|
|
163
|
+
token,
|
|
164
|
+
sAppId,
|
|
165
|
+
expiration,
|
|
166
|
+
bootstrap
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Record that a formation was completed successfully.
|
|
171
|
+
* Called after strand provisioning to track usage.
|
|
172
|
+
*/
|
|
173
|
+
async recordFormationComplete(token, initiatorKey, strandId) {
|
|
174
|
+
if (this.formationUsageRecorder) {
|
|
175
|
+
await this.formationUsageRecorder.recordUsage(token, initiatorKey, strandId);
|
|
176
|
+
log('Recorded formation usage: token=%s strand=%s', token, strandId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=strand-solicitation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strand-solicitation.js","sourceRoot":"","sources":["../src/strand-solicitation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { StrandFilter, StrandRow } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Extended strand row that includes the sAppId for filtering purposes.
|
|
4
|
+
* The sAppId is provided by the hosting application, not from the control network.
|
|
5
|
+
*/
|
|
6
|
+
export interface StrandRowWithApp extends StrandRow {
|
|
7
|
+
/** sApp ID if known (for filtering) */
|
|
8
|
+
sAppId?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Callback for strand changes
|
|
12
|
+
*/
|
|
13
|
+
export interface StrandWatcherCallbacks {
|
|
14
|
+
onStrandAdded: (strand: StrandRow) => Promise<void>;
|
|
15
|
+
onStrandRemoved: (strandId: string) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Interface for querying strands from control network
|
|
19
|
+
*/
|
|
20
|
+
export interface StrandQueryable {
|
|
21
|
+
queryStrands(): Promise<StrandRow[]>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Interface for looking up sAppId for a strand (for filtering)
|
|
25
|
+
*/
|
|
26
|
+
export interface SAppIdLookup {
|
|
27
|
+
/** Get the sAppId for a strand, if known */
|
|
28
|
+
getSAppId(strandId: string): string | undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Watches the control network's Strand table for changes and triggers
|
|
32
|
+
* strand instance start/stop via callbacks.
|
|
33
|
+
*
|
|
34
|
+
* Uses polling until Optimystic supports reactive subscriptions.
|
|
35
|
+
*/
|
|
36
|
+
export declare class StrandWatcher {
|
|
37
|
+
private readonly filter;
|
|
38
|
+
private readonly pollInterval;
|
|
39
|
+
private readonly callbacks;
|
|
40
|
+
private readonly queryable;
|
|
41
|
+
private readonly sAppIdLookup?;
|
|
42
|
+
private knownStrands;
|
|
43
|
+
private pollTimer;
|
|
44
|
+
private running;
|
|
45
|
+
constructor(queryable: StrandQueryable, callbacks: StrandWatcherCallbacks, filter?: StrandFilter, pollInterval?: number, sAppIdLookup?: SAppIdLookup);
|
|
46
|
+
/**
|
|
47
|
+
* Check if a strand passes the current filter
|
|
48
|
+
*/
|
|
49
|
+
private passesFilter;
|
|
50
|
+
/**
|
|
51
|
+
* Poll for strand changes
|
|
52
|
+
*/
|
|
53
|
+
private poll;
|
|
54
|
+
/**
|
|
55
|
+
* Start watching for strand changes
|
|
56
|
+
*/
|
|
57
|
+
start(): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Stop watching for strand changes
|
|
60
|
+
*/
|
|
61
|
+
stop(): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Get currently known strands
|
|
64
|
+
*/
|
|
65
|
+
getKnownStrands(): Map<string, StrandRow>;
|
|
66
|
+
/**
|
|
67
|
+
* Force an immediate poll (useful for testing)
|
|
68
|
+
*/
|
|
69
|
+
forcePoll(): Promise<void>;
|
|
70
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
const log = debug('sereus:cadre:strand-watcher');
|
|
3
|
+
/**
|
|
4
|
+
* Watches the control network's Strand table for changes and triggers
|
|
5
|
+
* strand instance start/stop via callbacks.
|
|
6
|
+
*
|
|
7
|
+
* Uses polling until Optimystic supports reactive subscriptions.
|
|
8
|
+
*/
|
|
9
|
+
export class StrandWatcher {
|
|
10
|
+
constructor(queryable, callbacks, filter = { mode: 'all' }, pollInterval = 5000, sAppIdLookup) {
|
|
11
|
+
this.knownStrands = new Map();
|
|
12
|
+
this.pollTimer = null;
|
|
13
|
+
this.running = false;
|
|
14
|
+
this.queryable = queryable;
|
|
15
|
+
this.callbacks = callbacks;
|
|
16
|
+
this.filter = filter;
|
|
17
|
+
this.pollInterval = pollInterval;
|
|
18
|
+
this.sAppIdLookup = sAppIdLookup;
|
|
19
|
+
log('StrandWatcher created with filter: %o, interval: %dms', filter, pollInterval);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if a strand passes the current filter
|
|
23
|
+
*/
|
|
24
|
+
passesFilter(strand) {
|
|
25
|
+
switch (this.filter.mode) {
|
|
26
|
+
case 'all':
|
|
27
|
+
return true;
|
|
28
|
+
case 'none':
|
|
29
|
+
return false;
|
|
30
|
+
case 'strandId':
|
|
31
|
+
return strand.Id === this.filter.strandId;
|
|
32
|
+
case 'sAppId': {
|
|
33
|
+
// Look up the sAppId for this strand
|
|
34
|
+
const sAppId = this.sAppIdLookup?.getSAppId(strand.Id);
|
|
35
|
+
if (sAppId === undefined) {
|
|
36
|
+
// If no sAppId lookup configured or unknown strand, pass through
|
|
37
|
+
// The strand will need to be re-evaluated when sApp info is available
|
|
38
|
+
log('sAppId unknown for strand %s - deferring filter decision', strand.Id);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const matches = sAppId === this.filter.sAppId;
|
|
42
|
+
log('sAppId filter: strand %s has sAppId %s, filter wants %s, match=%s', strand.Id, sAppId, this.filter.sAppId, matches);
|
|
43
|
+
return matches;
|
|
44
|
+
}
|
|
45
|
+
default:
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Poll for strand changes
|
|
51
|
+
*/
|
|
52
|
+
async poll() {
|
|
53
|
+
if (!this.running)
|
|
54
|
+
return;
|
|
55
|
+
try {
|
|
56
|
+
const currentStrands = await this.queryable.queryStrands();
|
|
57
|
+
const currentMap = new Map(currentStrands.map(s => [s.Id, s]));
|
|
58
|
+
// Find added strands
|
|
59
|
+
for (const strand of currentStrands) {
|
|
60
|
+
if (!this.knownStrands.has(strand.Id) && this.passesFilter(strand)) {
|
|
61
|
+
log('Strand added: %s', strand.Id);
|
|
62
|
+
this.knownStrands.set(strand.Id, strand);
|
|
63
|
+
try {
|
|
64
|
+
await this.callbacks.onStrandAdded(strand);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
log('Error handling strand add for %s: %o', strand.Id, error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Find removed strands
|
|
72
|
+
for (const [strandId] of this.knownStrands) {
|
|
73
|
+
if (!currentMap.has(strandId)) {
|
|
74
|
+
log('Strand removed: %s', strandId);
|
|
75
|
+
this.knownStrands.delete(strandId);
|
|
76
|
+
try {
|
|
77
|
+
await this.callbacks.onStrandRemoved(strandId);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
log('Error handling strand remove for %s: %o', strandId, error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
log('Error polling strands: %o', error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Start watching for strand changes
|
|
91
|
+
*/
|
|
92
|
+
async start() {
|
|
93
|
+
if (this.running) {
|
|
94
|
+
log('StrandWatcher already running');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
log('Starting StrandWatcher');
|
|
98
|
+
this.running = true;
|
|
99
|
+
// Do an initial poll immediately
|
|
100
|
+
await this.poll();
|
|
101
|
+
// Set up periodic polling
|
|
102
|
+
this.pollTimer = setInterval(() => {
|
|
103
|
+
void this.poll();
|
|
104
|
+
}, this.pollInterval);
|
|
105
|
+
log('StrandWatcher started');
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Stop watching for strand changes
|
|
109
|
+
*/
|
|
110
|
+
async stop() {
|
|
111
|
+
if (!this.running) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
log('Stopping StrandWatcher');
|
|
115
|
+
this.running = false;
|
|
116
|
+
if (this.pollTimer) {
|
|
117
|
+
clearInterval(this.pollTimer);
|
|
118
|
+
this.pollTimer = null;
|
|
119
|
+
}
|
|
120
|
+
this.knownStrands.clear();
|
|
121
|
+
log('StrandWatcher stopped');
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get currently known strands
|
|
125
|
+
*/
|
|
126
|
+
getKnownStrands() {
|
|
127
|
+
return new Map(this.knownStrands);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Force an immediate poll (useful for testing)
|
|
131
|
+
*/
|
|
132
|
+
async forcePoll() {
|
|
133
|
+
await this.poll();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=strand-watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strand-watcher.js","sourceRoot":"","sources":["../src/strand-watcher.ts"],"names":[],"mappings":"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"}
|