@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 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',
@@ -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 here on any early bail).
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
- // Get credentials for OAuth connectors
24
- const providerName = connector.auth.provider !== 'device' ? connector.auth.provider : null;
25
- const credential = providerName ? await credentialProvider.get(providerName, ownerDID) : null;
26
- if (providerName != null && credential == null) {
27
- release();
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: 'error'
130
+ status: 'started'
30
131
  };
31
- }
32
- // Ensure server provider is available
33
- if (connector.serverProvider == null) {
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.0",
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.0",
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.0",
25
- "@kubun/graphql": "^0.14.0",
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.0",
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.0",
37
- "@noble/hashes": "^2.3.0",
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",