@kubun/plugin-connector 0.14.0 → 0.14.1
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/lib/index.js +16 -1
- package/lib/sync/orchestrate.js +112 -76
- package/package.json +7 -7
package/lib/index.js
CHANGED
|
@@ -410,7 +410,22 @@ export function createConnectorPlugin(options) {
|
|
|
410
410
|
logger,
|
|
411
411
|
mutateDocuments: params.graph.mutateDocuments
|
|
412
412
|
});
|
|
413
|
-
})().catch(()=>{
|
|
413
|
+
})().catch((err)=>{
|
|
414
|
+
// Surface a deferred-run failure instead of swallowing it — the
|
|
415
|
+
// workflow enqueue and the fallback orchestrate both settle here.
|
|
416
|
+
// The emission itself rejects if a subscriber throws, so guard it:
|
|
417
|
+
// an unhandled rejection here could crash a strict host.
|
|
418
|
+
manager.emitSyncEvent({
|
|
419
|
+
type: 'error',
|
|
420
|
+
connectorName,
|
|
421
|
+
error: err instanceof Error ? err.message : String(err)
|
|
422
|
+
}).catch((emitErr)=>{
|
|
423
|
+
logger.warn('connector sync error event delivery failed {connectorName}', {
|
|
424
|
+
connectorName,
|
|
425
|
+
error: emitErr instanceof Error ? emitErr.message : String(emitErr)
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
});
|
|
414
429
|
});
|
|
415
430
|
return {
|
|
416
431
|
status: 'STARTED',
|
package/lib/sync/orchestrate.js
CHANGED
|
@@ -1,15 +1,40 @@
|
|
|
1
1
|
import { SyncEngine } from './engine.js';
|
|
2
2
|
export async function orchestrateSync(params) {
|
|
3
3
|
const { connectorName, full, registry, syncEventEmitter, stateStore, credentialProvider, ownerDID, stores, inFlight, logger } = params;
|
|
4
|
+
// A failed emission must never escape as an unhandled rejection. The event
|
|
5
|
+
// emitter rejects `emit()` when any subscriber throws, and these emits are
|
|
6
|
+
// either awaited on a bail path (where a throw would divert into the outer
|
|
7
|
+
// catch and deliver a second, misleading error) or floated on the
|
|
8
|
+
// fire-and-forget run chain (where a throw would crash a strict host). Swallow
|
|
9
|
+
// and log, so a misbehaving subscriber cannot corrupt the sync's own signalling.
|
|
10
|
+
const safeEmit = async (event)=>{
|
|
11
|
+
try {
|
|
12
|
+
await syncEventEmitter.emitSyncEvent(event);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
logger.warn('connector sync event delivery failed {connectorName}', {
|
|
15
|
+
connectorName,
|
|
16
|
+
error: err instanceof Error ? err.message : String(err)
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
// A failed sync must be observable. Every bail below emits an `error` event so a
|
|
21
|
+
// subscriber sees the failure instead of silence — a swallowed failure here
|
|
22
|
+
// starves concurrent connectors that never learn their sync was dropped.
|
|
23
|
+
const emitError = (error)=>safeEmit({
|
|
24
|
+
type: 'error',
|
|
25
|
+
connectorName,
|
|
26
|
+
error
|
|
27
|
+
});
|
|
4
28
|
const connector = registry.get(connectorName);
|
|
5
29
|
if (connector == null) {
|
|
30
|
+
await emitError('connector is not registered');
|
|
6
31
|
return {
|
|
7
32
|
status: 'error'
|
|
8
33
|
};
|
|
9
34
|
}
|
|
10
35
|
// Reserve the (connector, owner) synchronously — before any await — so two
|
|
11
36
|
// concurrent triggers cannot both pass the check. Released once the run
|
|
12
|
-
// settles (or
|
|
37
|
+
// settles (or on any early bail).
|
|
13
38
|
const guardKey = `${connectorName}:${ownerDID}`;
|
|
14
39
|
if (inFlight != null) {
|
|
15
40
|
if (inFlight.has(guardKey)) {
|
|
@@ -20,86 +45,97 @@ export async function orchestrateSync(params) {
|
|
|
20
45
|
inFlight.add(guardKey);
|
|
21
46
|
}
|
|
22
47
|
const release = ()=>inFlight?.delete(guardKey);
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
48
|
+
// Everything from the reservation to the engine handoff runs under one guard:
|
|
49
|
+
// any throw here (credential fetch, provider construction, the started emit)
|
|
50
|
+
// must release the reservation, or the guard would wedge every later sync into
|
|
51
|
+
// a silent `already_syncing`. The engine promise's own `.finally(release)` owns
|
|
52
|
+
// the guard only once `run()` has been handed off.
|
|
53
|
+
try {
|
|
54
|
+
// Get credentials for OAuth connectors
|
|
55
|
+
const providerName = connector.auth.provider !== 'device' ? connector.auth.provider : null;
|
|
56
|
+
const credential = providerName ? await credentialProvider.get(providerName, ownerDID) : null;
|
|
57
|
+
if (providerName != null && credential == null) {
|
|
58
|
+
release();
|
|
59
|
+
await emitError(`no credential for provider "${providerName}"`);
|
|
60
|
+
return {
|
|
61
|
+
status: 'error'
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Ensure server provider is available
|
|
65
|
+
if (connector.serverProvider == null) {
|
|
66
|
+
release();
|
|
67
|
+
await emitError('connector has no server provider');
|
|
68
|
+
return {
|
|
69
|
+
status: 'error'
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const effectiveBoundary = params.boundary ?? {
|
|
73
|
+
maxAge: 'P90D'
|
|
74
|
+
};
|
|
75
|
+
const provider = connector.serverProvider({
|
|
76
|
+
credential: credential ?? {
|
|
77
|
+
accessToken: '',
|
|
78
|
+
scopes: []
|
|
79
|
+
},
|
|
80
|
+
boundary: effectiveBoundary
|
|
81
|
+
});
|
|
82
|
+
const engine = new SyncEngine({
|
|
83
|
+
stores,
|
|
84
|
+
stateStore,
|
|
85
|
+
connectorName,
|
|
86
|
+
clusters: connector.clusters,
|
|
87
|
+
logger,
|
|
88
|
+
mutateDocuments: params.mutateDocuments
|
|
89
|
+
});
|
|
90
|
+
// Emit started event
|
|
91
|
+
await safeEmit({
|
|
92
|
+
type: 'started',
|
|
93
|
+
connectorName
|
|
94
|
+
});
|
|
95
|
+
// Bridge engine events to sync event emitter
|
|
96
|
+
engine.on('sync', (event)=>{
|
|
97
|
+
if (event.type === 'sync:progress') {
|
|
98
|
+
void safeEmit({
|
|
99
|
+
type: 'progress',
|
|
100
|
+
connectorName,
|
|
101
|
+
entitiesProcessed: event.entitiesProcessed,
|
|
102
|
+
entitiesFailed: event.entitiesFailed
|
|
103
|
+
});
|
|
104
|
+
} else if (event.type === 'sync:complete') {
|
|
105
|
+
void safeEmit({
|
|
106
|
+
type: 'completed',
|
|
107
|
+
connectorName,
|
|
108
|
+
totalProcessed: event.totalProcessed,
|
|
109
|
+
totalFailed: event.totalFailed,
|
|
110
|
+
duration: event.duration,
|
|
111
|
+
failedSample: event.failedSample
|
|
112
|
+
});
|
|
113
|
+
} else if (event.type === 'sync:error') {
|
|
114
|
+
void safeEmit({
|
|
115
|
+
type: 'error',
|
|
116
|
+
connectorName,
|
|
117
|
+
error: event.error,
|
|
118
|
+
failedSample: event.failedSample
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
// Run sync (fire-and-forget — mutation returns immediately)
|
|
123
|
+
engine.run({
|
|
124
|
+
provider,
|
|
125
|
+
ownerDID,
|
|
126
|
+
boundary: effectiveBoundary,
|
|
127
|
+
full
|
|
128
|
+
}).catch((err)=>emitError(err instanceof Error ? err.message : String(err))).finally(release);
|
|
28
129
|
return {
|
|
29
|
-
status: '
|
|
130
|
+
status: 'started'
|
|
30
131
|
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
132
|
+
} catch (err) {
|
|
133
|
+
// A pre-handoff throw: the engine never took ownership of the guard, so
|
|
134
|
+
// release it here and surface the failure.
|
|
34
135
|
release();
|
|
136
|
+
await emitError(err instanceof Error ? err.message : String(err));
|
|
35
137
|
return {
|
|
36
138
|
status: 'error'
|
|
37
139
|
};
|
|
38
140
|
}
|
|
39
|
-
const effectiveBoundary = params.boundary ?? {
|
|
40
|
-
maxAge: 'P90D'
|
|
41
|
-
};
|
|
42
|
-
const provider = connector.serverProvider({
|
|
43
|
-
credential: credential ?? {
|
|
44
|
-
accessToken: '',
|
|
45
|
-
scopes: []
|
|
46
|
-
},
|
|
47
|
-
boundary: effectiveBoundary
|
|
48
|
-
});
|
|
49
|
-
const engine = new SyncEngine({
|
|
50
|
-
stores,
|
|
51
|
-
stateStore,
|
|
52
|
-
connectorName,
|
|
53
|
-
clusters: connector.clusters,
|
|
54
|
-
logger,
|
|
55
|
-
mutateDocuments: params.mutateDocuments
|
|
56
|
-
});
|
|
57
|
-
// Emit started event
|
|
58
|
-
await syncEventEmitter.emitSyncEvent({
|
|
59
|
-
type: 'started',
|
|
60
|
-
connectorName
|
|
61
|
-
});
|
|
62
|
-
// Bridge engine events to sync event emitter
|
|
63
|
-
engine.on('sync', (event)=>{
|
|
64
|
-
if (event.type === 'sync:progress') {
|
|
65
|
-
syncEventEmitter.emitSyncEvent({
|
|
66
|
-
type: 'progress',
|
|
67
|
-
connectorName,
|
|
68
|
-
entitiesProcessed: event.entitiesProcessed,
|
|
69
|
-
entitiesFailed: event.entitiesFailed
|
|
70
|
-
});
|
|
71
|
-
} else if (event.type === 'sync:complete') {
|
|
72
|
-
syncEventEmitter.emitSyncEvent({
|
|
73
|
-
type: 'completed',
|
|
74
|
-
connectorName,
|
|
75
|
-
totalProcessed: event.totalProcessed,
|
|
76
|
-
totalFailed: event.totalFailed,
|
|
77
|
-
duration: event.duration,
|
|
78
|
-
failedSample: event.failedSample
|
|
79
|
-
});
|
|
80
|
-
} else if (event.type === 'sync:error') {
|
|
81
|
-
syncEventEmitter.emitSyncEvent({
|
|
82
|
-
type: 'error',
|
|
83
|
-
connectorName,
|
|
84
|
-
error: event.error,
|
|
85
|
-
failedSample: event.failedSample
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
// Run sync (fire-and-forget — mutation returns immediately)
|
|
90
|
-
engine.run({
|
|
91
|
-
provider,
|
|
92
|
-
ownerDID,
|
|
93
|
-
boundary: effectiveBoundary,
|
|
94
|
-
full
|
|
95
|
-
}).catch(async (err)=>{
|
|
96
|
-
await syncEventEmitter.emitSyncEvent({
|
|
97
|
-
type: 'error',
|
|
98
|
-
connectorName,
|
|
99
|
-
error: err instanceof Error ? err.message : String(err)
|
|
100
|
-
});
|
|
101
|
-
}).finally(release);
|
|
102
|
-
return {
|
|
103
|
-
status: 'started'
|
|
104
|
-
};
|
|
105
141
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/plugin-connector",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"license": "see LICENSE.md",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -18,23 +18,23 @@
|
|
|
18
18
|
"@kokuin/jwe": "^0.1.0",
|
|
19
19
|
"@kokuin/token": "^0.5.0",
|
|
20
20
|
"@kubun/connector": "^0.14.0",
|
|
21
|
-
"@kubun/credential": "^0.14.
|
|
21
|
+
"@kubun/credential": "^0.14.1",
|
|
22
22
|
"@kubun/credential-types": "^0.14.0",
|
|
23
23
|
"@kubun/db": "^0.14.0",
|
|
24
|
-
"@kubun/engine": "^0.14.
|
|
25
|
-
"@kubun/graphql": "^0.14.
|
|
24
|
+
"@kubun/engine": "^0.14.1",
|
|
25
|
+
"@kubun/graphql": "^0.14.1",
|
|
26
26
|
"@kubun/hlc": "^0.14.0",
|
|
27
27
|
"@kubun/id": "^0.14.0",
|
|
28
28
|
"@kubun/logger": "^0.14.0",
|
|
29
29
|
"@kubun/plugin-blob-api": "^0.14.0",
|
|
30
30
|
"@kubun/plugin-workflow-api": "^0.14.0",
|
|
31
|
-
"@kubun/protocol": "^0.14.
|
|
31
|
+
"@kubun/protocol": "^0.14.1",
|
|
32
32
|
"@kubun/service-credential-api": "^0.14.0",
|
|
33
33
|
"@kubun/store-connector": "^0.14.0",
|
|
34
34
|
"@kubun/store-credential": "^0.14.0",
|
|
35
35
|
"@kubun/store-delegation": "^0.14.0",
|
|
36
|
-
"@kubun/store-graph": "^0.14.
|
|
37
|
-
"@noble/hashes": "^2.
|
|
36
|
+
"@kubun/store-graph": "^0.14.1",
|
|
37
|
+
"@noble/hashes": "^2.4.0",
|
|
38
38
|
"@sozai/async": "^0.2.1",
|
|
39
39
|
"@sozai/codec": "^0.4.0",
|
|
40
40
|
"@sozai/event": "^0.1.3",
|