hikoutei 0.8.1 → 0.8.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 +4 -0
- package/dist/adapter/sheets/providers/google-sheets-api/GoogleSheetsApiSyncProvider.d.ts +23 -11
- package/dist/adapter/sheets/providers/google-sheets-api/GoogleSheetsApiSyncProvider.d.ts.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/GoogleSheetsApiSyncProvider.js +26 -18
- package/dist/adapter/sheets/providers/google-sheets-api/GoogleSheetsApiSyncProvider.js.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/constants.d.ts +24 -5
- package/dist/adapter/sheets/providers/google-sheets-api/constants.d.ts.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/constants.js +24 -5
- package/dist/adapter/sheets/providers/google-sheets-api/constants.js.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/errors.d.ts +8 -0
- package/dist/adapter/sheets/providers/google-sheets-api/errors.d.ts.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/errors.js +20 -0
- package/dist/adapter/sheets/providers/google-sheets-api/errors.js.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/index.d.ts +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/index.d.ts.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/index.js +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/index.js.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/model/valueNormalization.d.ts +11 -1
- package/dist/adapter/sheets/providers/google-sheets-api/model/valueNormalization.d.ts.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/model/valueNormalization.js +12 -2
- package/dist/adapter/sheets/providers/google-sheets-api/model/valueNormalization.js.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/operations/shared.d.ts +19 -6
- package/dist/adapter/sheets/providers/google-sheets-api/operations/shared.d.ts.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/operations/shared.js +52 -10
- package/dist/adapter/sheets/providers/google-sheets-api/operations/shared.js.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/transport/googleSheetsApiTransport.d.ts +13 -0
- package/dist/adapter/sheets/providers/google-sheets-api/transport/googleSheetsApiTransport.d.ts.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/transport/googleSheetsApiTransport.js +51 -3
- package/dist/adapter/sheets/providers/google-sheets-api/transport/googleSheetsApiTransport.js.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/transport/rateLimiter.d.ts +40 -4
- package/dist/adapter/sheets/providers/google-sheets-api/transport/rateLimiter.d.ts.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/transport/rateLimiter.js +26 -20
- package/dist/adapter/sheets/providers/google-sheets-api/transport/rateLimiter.js.map +1 -1
- package/dist/api/Hikoutei.d.ts.map +1 -1
- package/dist/api/Hikoutei.js +244 -30
- package/dist/api/Hikoutei.js.map +1 -1
- package/dist/api/internalEntityManager.d.ts.map +1 -1
- package/dist/api/internalEntityManager.js +152 -53
- package/dist/api/internalEntityManager.js.map +1 -1
- package/dist/application/sync/outbound/SheetsEffectDispatcher.d.ts +41 -0
- package/dist/application/sync/outbound/SheetsEffectDispatcher.d.ts.map +1 -1
- package/dist/application/sync/outbound/SheetsEffectDispatcher.js +85 -3
- package/dist/application/sync/outbound/SheetsEffectDispatcher.js.map +1 -1
- package/dist/application/sync/outbound/reconciliation/ReconciliationScanner.d.ts.map +1 -1
- package/dist/application/sync/outbound/reconciliation/ReconciliationScanner.js +39 -5
- package/dist/application/sync/outbound/reconciliation/ReconciliationScanner.js.map +1 -1
- package/dist/application/sync/outbound/reconciliation/diff.d.ts +19 -1
- package/dist/application/sync/outbound/reconciliation/diff.d.ts.map +1 -1
- package/dist/application/sync/outbound/reconciliation/diff.js +60 -17
- package/dist/application/sync/outbound/reconciliation/diff.js.map +1 -1
- package/dist/application/sync/outbound/reconciliation/repair.d.ts +34 -3
- package/dist/application/sync/outbound/reconciliation/repair.d.ts.map +1 -1
- package/dist/application/sync/outbound/reconciliation/repair.js +120 -47
- package/dist/application/sync/outbound/reconciliation/repair.js.map +1 -1
- package/dist/application/sync/outbound/reconciliation/shared.d.ts +20 -0
- package/dist/application/sync/outbound/reconciliation/shared.d.ts.map +1 -1
- package/dist/application/sync/outbound/reconciliation/shared.js +35 -0
- package/dist/application/sync/outbound/reconciliation/shared.js.map +1 -1
- package/dist/application/sync/service/SyncPollingSupervisor.d.ts +16 -0
- package/dist/application/sync/service/SyncPollingSupervisor.d.ts.map +1 -1
- package/dist/application/sync/service/SyncPollingSupervisor.js +24 -0
- package/dist/application/sync/service/SyncPollingSupervisor.js.map +1 -1
- package/dist/application/sync/service/SyncServiceBootstrap.d.ts.map +1 -1
- package/dist/application/sync/service/SyncServiceBootstrap.js +26 -1
- package/dist/application/sync/service/SyncServiceBootstrap.js.map +1 -1
- package/dist/application/sync/service/effectSupervisor.d.ts.map +1 -1
- package/dist/application/sync/service/effectSupervisor.js +114 -9
- package/dist/application/sync/service/effectSupervisor.js.map +1 -1
- package/dist/application/sync/service/pollingSupervisor.d.ts +17 -0
- package/dist/application/sync/service/pollingSupervisor.d.ts.map +1 -1
- package/dist/application/sync/service/pollingSupervisor.js +103 -3
- package/dist/application/sync/service/pollingSupervisor.js.map +1 -1
- package/dist/application/sync/service/serviceOptions.d.ts.map +1 -1
- package/dist/application/sync/service/serviceOptions.js +28 -18
- package/dist/application/sync/service/serviceOptions.js.map +1 -1
- package/dist/application/sync/service/shutdown.d.ts.map +1 -1
- package/dist/application/sync/service/shutdown.js +6 -2
- package/dist/application/sync/service/shutdown.js.map +1 -1
- package/dist/application/sync/service/syncAutoStart.d.ts +71 -6
- package/dist/application/sync/service/syncAutoStart.d.ts.map +1 -1
- package/dist/application/sync/service/syncAutoStart.js +190 -23
- package/dist/application/sync/service/syncAutoStart.js.map +1 -1
- package/dist/application/sync/service/systemStateReadiness.d.ts +35 -0
- package/dist/application/sync/service/systemStateReadiness.d.ts.map +1 -0
- package/dist/application/sync/service/systemStateReadiness.js +51 -0
- package/dist/application/sync/service/systemStateReadiness.js.map +1 -0
- package/dist/application/sync/sheetsContract/mutationCoordinator/CoordinatedSheetsProvider.d.ts +37 -1
- package/dist/application/sync/sheetsContract/mutationCoordinator/CoordinatedSheetsProvider.d.ts.map +1 -1
- package/dist/application/sync/sheetsContract/mutationCoordinator/CoordinatedSheetsProvider.js +57 -3
- package/dist/application/sync/sheetsContract/mutationCoordinator/CoordinatedSheetsProvider.js.map +1 -1
- package/dist/application/sync/sheetsContract/transportOutcome.d.ts +32 -5
- package/dist/application/sync/sheetsContract/transportOutcome.d.ts.map +1 -1
- package/dist/application/sync/sheetsContract/transportOutcome.js +101 -7
- package/dist/application/sync/sheetsContract/transportOutcome.js.map +1 -1
- package/dist/internal/syncStatus.js +33 -2
- package/dist/internal/syncStatus.js.map +1 -1
- package/dist/shared/observability/index.d.ts +9 -0
- package/dist/shared/observability/index.d.ts.map +1 -0
- package/dist/shared/observability/index.js +9 -0
- package/dist/shared/observability/index.js.map +1 -0
- package/dist/shared/observability/internalLog.d.ts +175 -0
- package/dist/shared/observability/internalLog.d.ts.map +1 -0
- package/dist/shared/observability/internalLog.js +471 -0
- package/dist/shared/observability/internalLog.js.map +1 -0
- package/dist/shared/observability/logEvents.d.ts +96 -0
- package/dist/shared/observability/logEvents.d.ts.map +1 -0
- package/dist/shared/observability/logEvents.js +247 -0
- package/dist/shared/observability/logEvents.js.map +1 -0
- package/package.json +5 -4
|
@@ -15,7 +15,6 @@ import { randomUUID } from "node:crypto";
|
|
|
15
15
|
import { GOOGLE_SHEETS_API_DEFAULTS } from "../../../adapter/sheets/providers/google-sheets-api/constants.js";
|
|
16
16
|
import { DEFAULT_EFFECT_LEASE_DURATION_MS, DEFAULT_WRITER_LEASE_DURATION_MS, EFFECT_LEASE_PROVIDER_HEADROOM_MS, } from "@hikoutei/ikisaki";
|
|
17
17
|
import { SYNC_SERVICE_ERROR_CODES, SyncServiceError, } from "./errors.js";
|
|
18
|
-
import { internalPositiveSafeIntegerSchema, internalSyncRouteSchema, internalSyncTextSchema, } from "./serviceOptionSchemas.js";
|
|
19
18
|
/**
|
|
20
19
|
* Maps a structured descriptor-registry failure to the internal
|
|
21
20
|
* `SyncServiceError` contract with unchanged `INVALID_OPTIONS` messages.
|
|
@@ -31,16 +30,16 @@ export function throwSyncResolutionError(failure) {
|
|
|
31
30
|
}
|
|
32
31
|
}
|
|
33
32
|
export function validateServiceOptions(options, descriptors) {
|
|
34
|
-
if (
|
|
33
|
+
if (options.dbName.trim() === "") {
|
|
35
34
|
throw new SyncServiceError(SYNC_SERVICE_ERROR_CODES.INVALID_OPTIONS, "sync service dbName is required.");
|
|
36
35
|
}
|
|
37
36
|
validateEffectLeaseHeadroom(options);
|
|
38
37
|
if (options.pollingFullScanIntervalMs !== undefined &&
|
|
39
|
-
!
|
|
38
|
+
(!Number.isSafeInteger(options.pollingFullScanIntervalMs) || options.pollingFullScanIntervalMs < 1)) {
|
|
40
39
|
throw new SyncServiceError(SYNC_SERVICE_ERROR_CODES.INVALID_OPTIONS, "sync service pollingFullScanIntervalMs must be a positive safe integer.");
|
|
41
40
|
}
|
|
42
41
|
if (options.reconciliationIntervalMs !== undefined &&
|
|
43
|
-
!
|
|
42
|
+
(!Number.isSafeInteger(options.reconciliationIntervalMs) || options.reconciliationIntervalMs < 1)) {
|
|
44
43
|
throw new SyncServiceError(SYNC_SERVICE_ERROR_CODES.INVALID_OPTIONS, "sync service reconciliationIntervalMs must be a positive safe integer.");
|
|
45
44
|
}
|
|
46
45
|
requireText(options.projections.spreadsheetId, "sync service spreadsheetId");
|
|
@@ -72,7 +71,8 @@ export function validateServiceOptions(options, descriptors) {
|
|
|
72
71
|
}
|
|
73
72
|
function validateEffectLeaseHeadroom(options) {
|
|
74
73
|
const effectLeaseDurationMs = options.effectLeaseDurationMs ?? DEFAULT_EFFECT_LEASE_DURATION_MS;
|
|
75
|
-
if (!
|
|
74
|
+
if (!Number.isSafeInteger(effectLeaseDurationMs) ||
|
|
75
|
+
effectLeaseDurationMs < 1) {
|
|
76
76
|
throw new SyncServiceError(SYNC_SERVICE_ERROR_CODES.INVALID_OPTIONS, "sync service effectLeaseDurationMs must be a positive safe integer.");
|
|
77
77
|
}
|
|
78
78
|
if (effectLeaseDurationMs >= DEFAULT_WRITER_LEASE_DURATION_MS) {
|
|
@@ -92,10 +92,17 @@ function validateEffectLeaseHeadroom(options) {
|
|
|
92
92
|
throw new SyncServiceError(SYNC_SERVICE_ERROR_CODES.INVALID_OPTIONS, "sync service googleSheetsApi requestTimeoutMs must be between 1 second and 120 seconds.");
|
|
93
93
|
}
|
|
94
94
|
// A direct-mode dispatch performs up to THREE sequential paced transport
|
|
95
|
-
// calls (two preflight reads plus one write), each with its
|
|
96
|
-
// the lease must cover the whole sequence
|
|
97
|
-
//
|
|
98
|
-
//
|
|
95
|
+
// calls (two preflight/postcondition reads plus one write), each with its
|
|
96
|
+
// own timeout; the lease must cover the whole sequence. The request-start
|
|
97
|
+
// limiter keeps consecutive starts at least `intervalMs` apart, and a
|
|
98
|
+
// dispatch can wait up to one FULL interval for its first slot because
|
|
99
|
+
// the shared limiter may hold a prior reservation, so the paced worst
|
|
100
|
+
// case is the write timeout plus ONE interval (the first-slot wait) plus
|
|
101
|
+
// TWO more slots of the LARGER of the read timeout and the interval (a
|
|
102
|
+
// read duration covers its own interval wait when it is the larger term,
|
|
103
|
+
// and the interval covers it when it is not; the two never add on top of
|
|
104
|
+
// each other). Defaults with the 2,500 ms interval: 60 + 2.5 + 2x10 + 30
|
|
105
|
+
// = 112.5 s, inside the 120 s default effect lease.
|
|
99
106
|
const readTimeoutMs = options.googleSheetsApi.readTimeoutMs
|
|
100
107
|
?? GOOGLE_SHEETS_API_DEFAULTS.READ_TIMEOUT_MS;
|
|
101
108
|
if (!Number.isSafeInteger(readTimeoutMs) ||
|
|
@@ -103,9 +110,16 @@ function validateEffectLeaseHeadroom(options) {
|
|
|
103
110
|
readTimeoutMs > GOOGLE_SHEETS_API_DEFAULTS.MAX_READ_TIMEOUT_MS) {
|
|
104
111
|
throw new SyncServiceError(SYNC_SERVICE_ERROR_CODES.INVALID_OPTIONS, "sync service googleSheetsApi readTimeoutMs must be between 1 second and 60 seconds.");
|
|
105
112
|
}
|
|
106
|
-
|
|
113
|
+
const intervalMs = options.googleSheetsApi.rateLimitIntervalMs
|
|
114
|
+
?? GOOGLE_SHEETS_API_DEFAULTS.REQUEST_START_INTERVAL_MS;
|
|
115
|
+
if (!Number.isSafeInteger(intervalMs) || intervalMs < 0) {
|
|
116
|
+
throw new SyncServiceError(SYNC_SERVICE_ERROR_CODES.INVALID_OPTIONS, "sync service googleSheetsApi rateLimitIntervalMs must be a non-negative safe integer.");
|
|
117
|
+
}
|
|
118
|
+
if (effectLeaseDurationMs <= requestTimeoutMs +
|
|
119
|
+
intervalMs +
|
|
120
|
+
2 * Math.max(readTimeoutMs, intervalMs) +
|
|
107
121
|
EFFECT_LEASE_PROVIDER_HEADROOM_MS) {
|
|
108
|
-
throw new SyncServiceError(SYNC_SERVICE_ERROR_CODES.INVALID_OPTIONS, "sync service effectLeaseDurationMs must exceed Google Sheets API requestTimeoutMs plus two
|
|
122
|
+
throw new SyncServiceError(SYNC_SERVICE_ERROR_CODES.INVALID_OPTIONS, "sync service effectLeaseDurationMs must exceed Google Sheets API requestTimeoutMs plus the request-start interval and two paced request slots (the larger of readTimeoutMs and the request-start interval) by 30 seconds before supervisors start.");
|
|
109
123
|
}
|
|
110
124
|
}
|
|
111
125
|
function validateEntityConfig(entityName, descriptor, config, routes, spreadsheetId) {
|
|
@@ -130,12 +144,8 @@ function validateRoute(entityName, projection, route) {
|
|
|
130
144
|
if (route === null || typeof route !== "object") {
|
|
131
145
|
throwInvalidProjection(`sync route ${entityName}.${projection} must be an object.`);
|
|
132
146
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const candidate = route;
|
|
136
|
-
requireText(candidate.tabName, `sync route ${entityName}.${projection}.tabName`);
|
|
137
|
-
requireText(candidate.registeredRange, `sync route ${entityName}.${projection}.registeredRange`);
|
|
138
|
-
}
|
|
147
|
+
requireText(route.tabName, `sync route ${entityName}.${projection}.tabName`);
|
|
148
|
+
requireText(route.registeredRange, `sync route ${entityName}.${projection}.registeredRange`);
|
|
139
149
|
}
|
|
140
150
|
function addRoute(routes, spreadsheetId, tabName, entityName, projection) {
|
|
141
151
|
const key = JSON.stringify([spreadsheetId, tabName.trim()]);
|
|
@@ -151,7 +161,7 @@ export function createWriterOptions(options) {
|
|
|
151
161
|
};
|
|
152
162
|
}
|
|
153
163
|
function requireText(value, label) {
|
|
154
|
-
if (
|
|
164
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
155
165
|
throwInvalidProjection(`${label} must be a non-empty string.`);
|
|
156
166
|
}
|
|
157
167
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serviceOptions.js","sourceRoot":"","sources":["../../../../src/application/sync/service/serviceOptions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAiBzC,OAAO,EAAE,0BAA0B,EAAE,MAAM,kEAAkE,CAAC;AAC9G,OAAO,EACL,gCAAgC,EAChC,gCAAgC,EAChC,iCAAiC,GAGlC,MAAM,mBAAmB,CAAC;AAK3B,OAAO,EACL,wBAAwB,EACxB,gBAAgB,GACjB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"serviceOptions.js","sourceRoot":"","sources":["../../../../src/application/sync/service/serviceOptions.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shutdown.d.ts","sourceRoot":"","sources":["../../../../src/application/sync/service/shutdown.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAEL,KAAK,8BAA8B,EACpC,MAAM,4CAA4C,CAAC;AACpD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mFAAmF,CAAC;AAC/H,OAAO,
|
|
1
|
+
{"version":3,"file":"shutdown.d.ts","sourceRoot":"","sources":["../../../../src/application/sync/service/shutdown.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAEL,KAAK,8BAA8B,EACpC,MAAM,4CAA4C,CAAC;AACpD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mFAAmF,CAAC;AAC/H,OAAO,EAKL,KAAK,sBAAsB,EAC5B,MAAM,mBAAmB,CAAC;AAG3B,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,0FAA0F,CAAC;AAC7I,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAExE;;;;;;;;;GASG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,8BAA8B,EACtC,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CAgDf;AAED,6DAA6D;AAC7D,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,8BAA8B,CAAC;IAChD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,iBAAiB,EAAE,qBAAqB,CAAC,4BAA4B,CAAC,CAAC;IAChF,QAAQ,CAAC,gBAAgB,EAAE,sBAAsB,CAAC;CACnD;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAqCpF"}
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* releases mirrored from every claim site wired into the bootstrap.
|
|
8
8
|
*/
|
|
9
9
|
import { DEFAULT_MAPPED_WRITER_ROLE, } from "../../orm/persistence/support/contracts.js";
|
|
10
|
-
import { DEFAULT_WORKER_ROLE, LOOKUP_RESULT_KINDS, readWriterLeaseWithAdapter, releaseWriterLeaseWithAdapter,
|
|
10
|
+
import { DEFAULT_WORKER_ROLE, LOOKUP_RESULT_KINDS, readWriterLeaseWithAdapter, releaseWriterLeaseWithAdapter, } from "@hikoutei/ikisaki";
|
|
11
11
|
import { RECONCILIATION_DEFAULTS } from "../outbound/reconciliation/ReconciliationScanner.js";
|
|
12
|
+
import { stableConsoleErrorTag } from "../../../shared/observability/internalLog.js";
|
|
12
13
|
/**
|
|
13
14
|
* Expires this runtime's own writer leases on graceful shutdown.
|
|
14
15
|
*
|
|
@@ -58,7 +59,10 @@ export async function expireRuntimeWriterLeases(storage, writer, effectWorkerId)
|
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
catch (error) {
|
|
61
|
-
|
|
62
|
+
// Default console diagnostics emit only the stable allowlisted
|
|
63
|
+
// class/code tag — never the raw message, which can embed provider
|
|
64
|
+
// payload fragments, spreadsheet IDs, emails, or paths.
|
|
65
|
+
console.warn(`[sync-service] writer lease release failed for role "${role}": ${stableConsoleErrorTag(error)}`);
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shutdown.js","sourceRoot":"","sources":["../../../../src/application/sync/service/shutdown.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,0BAA0B,GAE3B,MAAM,4CAA4C,CAAC;AAEpD,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,0BAA0B,EAC1B,6BAA6B,
|
|
1
|
+
{"version":3,"file":"shutdown.js","sourceRoot":"","sources":["../../../../src/application/sync/service/shutdown.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This internal module owns the mapping from environment variables to the
|
|
5
5
|
* sync service bootstrap: spreadsheet URL parsing, service-account credentials
|
|
6
|
-
* file validation, polling interval parsing,
|
|
7
|
-
* entity descriptors, and fail-closed
|
|
8
|
-
* never re-exported from `src/index.ts`;
|
|
9
|
-
*
|
|
10
|
-
* sync module graph.
|
|
6
|
+
* file validation, polling interval parsing, request-start pacing override
|
|
7
|
+
* parsing, projection auto-generation from entity descriptors, and fail-closed
|
|
8
|
+
* startup failure classification. It is never re-exported from `src/index.ts`;
|
|
9
|
+
* the public factory calls it only when `HIKOUTEI_SYNC_SPREADSHEET_URL` is
|
|
10
|
+
* set, so local-only users never load the sync module graph.
|
|
11
11
|
*
|
|
12
12
|
* The env reader, transport, and diagnostic sink are injectable so tests can
|
|
13
13
|
* exercise every startup branch with a stub transport, a fake env, and a
|
|
@@ -35,14 +35,46 @@ export declare const SYNC_ENV_KEYS: {
|
|
|
35
35
|
readonly POLLING_INTERVAL_MS: "HIKOUTEI_SYNC_POLLING_INTERVAL_MS";
|
|
36
36
|
/** Optional metadata safety full-scan cadence in ms; defaults to 60 seconds. */
|
|
37
37
|
readonly FULL_SCAN_INTERVAL_MS: "HIKOUTEI_SYNC_FULL_SCAN_INTERVAL_MS";
|
|
38
|
+
/**
|
|
39
|
+
* Optional request-start pacing in ms for the direct provider's ONE shared
|
|
40
|
+
* read+write limiter; absent uses the safe default (2,500 ms). Internal
|
|
41
|
+
* only — never part of the root public API.
|
|
42
|
+
*/
|
|
43
|
+
readonly RATE_LIMIT_INTERVAL_MS: "HIKOUTEI_SYNC_RATE_LIMIT_INTERVAL_MS";
|
|
38
44
|
};
|
|
39
45
|
/** Default polling cadence applied when the interval env vars are absent. */
|
|
40
46
|
export declare const DEFAULT_SYNC_POLLING_INTERVAL_MS = 60000;
|
|
41
47
|
/** Default safety full-scan cadence applied when the interval env vars are absent. */
|
|
42
48
|
export declare const DEFAULT_SYNC_FULL_SCAN_INTERVAL_MS = 60000;
|
|
49
|
+
/** Lower bound for the sync request-start pacing env override (1 second). */
|
|
50
|
+
export declare const MIN_SYNC_RATE_LIMIT_INTERVAL_MS = 1000;
|
|
51
|
+
/**
|
|
52
|
+
* Upper bound for the sync request-start pacing env override.
|
|
53
|
+
*
|
|
54
|
+
* A worst-case effect dispatch performs THREE sequential paced transport
|
|
55
|
+
* calls (two preflight/postcondition reads plus one batch write). The effect
|
|
56
|
+
* lease must still cover the whole sequence with the 30-second provider
|
|
57
|
+
* headroom, and a dispatch can wait up to one FULL interval for its first
|
|
58
|
+
* slot because the shared limiter may hold a prior reservation — admission
|
|
59
|
+
* is BOUNDED to one interval, so a request whose slot lies further out is
|
|
60
|
+
* refused before any SDK call (delivery-uncertain, requeued durably)
|
|
61
|
+
* instead of waiting past the lease. With the DEFAULT lease, write timeout,
|
|
62
|
+
* and read timeout the interval must satisfy
|
|
63
|
+
* `120s > 60s + I + 2 * max(10s, I) + 30s`, i.e. `I < 10s`. Below
|
|
64
|
+
* the 10 s read timeout the two read slots cost 2 x 10 s and the interval
|
|
65
|
+
* adds once, so the bound is `I < 120 - 60 - 2x10 - 30 = 10 s`; at or
|
|
66
|
+
* above the read timeout the interval dominates (`3I < 30s` has no
|
|
67
|
+
* solution at or above 10 s), so the below-read-slot branch is binding.
|
|
68
|
+
* The ceiling below is derived from those defaults (10,000 - 1 = 9,999 ms)
|
|
69
|
+
* and the service-level lease-headroom validation applies the same math to
|
|
70
|
+
* the ACTIVE lease and timeouts, so an override that could let pacing push
|
|
71
|
+
* a dispatch past the lease is rejected with a stable startup failure
|
|
72
|
+
* instead of risking lease expiry and duplicate remote delivery.
|
|
73
|
+
*/
|
|
74
|
+
export declare const MAX_SYNC_RATE_LIMIT_INTERVAL_MS: number;
|
|
43
75
|
/** Diagnostic log levels emitted by the auto-start bridge. */
|
|
44
76
|
export type SyncDiagnosticLevel = "info" | "error";
|
|
45
|
-
/** Diagnostic sink; defaults to console.
|
|
77
|
+
/** Diagnostic sink; defaults to console. Receives only stable class/code summaries, never full failure messages. */
|
|
46
78
|
export type SyncDiagnostic = (level: SyncDiagnosticLevel, message: string) => void;
|
|
47
79
|
/** Internal auto-start options; none are part of the root application contract. */
|
|
48
80
|
export interface SyncAutoStartOptions {
|
|
@@ -125,4 +157,37 @@ export declare function buildSyncProjections(entities: readonly HikouteiEntity[]
|
|
|
125
157
|
* else (timeout, network, invalid options) maps to the generic startup code.
|
|
126
158
|
*/
|
|
127
159
|
export declare function classifySyncStartupFailure(error: unknown, spreadsheetId: string, clientEmail: string): HikouteiError;
|
|
160
|
+
/**
|
|
161
|
+
* Resolves the sync request-start pacing env override, or undefined.
|
|
162
|
+
*
|
|
163
|
+
* `HIKOUTEI_SYNC_RATE_LIMIT_INTERVAL_MS` is the internal override for the
|
|
164
|
+
* direct provider's ONE shared read+write request-start limiter. Absent or
|
|
165
|
+
* blank means the provider's safe default (2,500 ms) applies; a present
|
|
166
|
+
* value must be a plain decimal integer between 1,000 ms and
|
|
167
|
+
* `MAX_SYNC_RATE_LIMIT_INTERVAL_MS` (~10 s). The ceiling is the largest
|
|
168
|
+
* interval whose worst-case three-request paced dispatch still finishes
|
|
169
|
+
* inside the default effect lease (120 s) with the default write timeout
|
|
170
|
+
* (60 s), read timeout (10 s), and provider headroom (30 s): a dispatch
|
|
171
|
+
* can wait up to one full interval for its first slot because the shared
|
|
172
|
+
* limiter may hold a prior reservation, so the strict default-safe bound
|
|
173
|
+
* is `120s > 60s + I + 2 * max(10s, I) + 30s` and a larger interval could
|
|
174
|
+
* let pacing push the dispatch past the lease and risk duplicate remote
|
|
175
|
+
* delivery. Values outside those bounds (including hex,
|
|
176
|
+
* exponent, signed, padded, fractional, and non-numeric forms) fail closed
|
|
177
|
+
* with the stable startup code so a mistyped override can never silently
|
|
178
|
+
* drop pacing to a burst or outlive the effect lease. The key stays internal
|
|
179
|
+
* and is never part of the root public API.
|
|
180
|
+
*/
|
|
181
|
+
export declare function resolveSyncRateLimitIntervalMs(env: Readonly<Record<string, string | undefined>>): number | undefined;
|
|
182
|
+
/**
|
|
183
|
+
* Stable redacted startup summary for diagnostic sinks: class and code
|
|
184
|
+
* only, each checked against the shared stable allowlists.
|
|
185
|
+
*
|
|
186
|
+
* `failure.name` and `failure.code` are runtime strings that could carry
|
|
187
|
+
* secret-like text (paths, emails, tokens) if an unexpected error shape
|
|
188
|
+
* ever reached the sink path, so unknown or malformed values collapse to
|
|
189
|
+
* the fixed `unknown` class/code. The full public message is carried by
|
|
190
|
+
* the thrown HikouteiError alone and never reaches any sink.
|
|
191
|
+
*/
|
|
192
|
+
export declare function stableStartupDiagnostic(failure: HikouteiError): string;
|
|
128
193
|
//# sourceMappingURL=syncAutoStart.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncAutoStart.d.ts","sourceRoot":"","sources":["../../../../src/application/sync/service/syncAutoStart.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;
|
|
1
|
+
{"version":3,"file":"syncAutoStart.d.ts","sourceRoot":"","sources":["../../../../src/application/sync/service/syncAutoStart.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This internal module owns the mapping from environment variables to the
|
|
5
5
|
* sync service bootstrap: spreadsheet URL parsing, service-account credentials
|
|
6
|
-
* file validation, polling interval parsing,
|
|
7
|
-
* entity descriptors, and fail-closed
|
|
8
|
-
* never re-exported from `src/index.ts`;
|
|
9
|
-
*
|
|
10
|
-
* sync module graph.
|
|
6
|
+
* file validation, polling interval parsing, request-start pacing override
|
|
7
|
+
* parsing, projection auto-generation from entity descriptors, and fail-closed
|
|
8
|
+
* startup failure classification. It is never re-exported from `src/index.ts`;
|
|
9
|
+
* the public factory calls it only when `HIKOUTEI_SYNC_SPREADSHEET_URL` is
|
|
10
|
+
* set, so local-only users never load the sync module graph.
|
|
11
11
|
*
|
|
12
12
|
* The env reader, transport, and diagnostic sink are injectable so tests can
|
|
13
13
|
* exercise every startup branch with a stub transport, a fake env, and a
|
|
@@ -20,17 +20,19 @@
|
|
|
20
20
|
* failure never leaves a half-open runtime behind.
|
|
21
21
|
*/
|
|
22
22
|
import { readFile } from "node:fs/promises";
|
|
23
|
-
import { z } from "zod";
|
|
24
23
|
import { getEntityDescriptor, } from "../../../api/entity.js";
|
|
25
24
|
import { createLocalTypedSheetsRuntime, validateTypedSheetsOptions, } from "../../../api/Hikoutei.js";
|
|
26
25
|
import { HIKOUTEI_ERROR_CODES, HikouteiError, } from "../../../api/errors.js";
|
|
27
26
|
import { GoogleSheetsApiTransportError, } from "../../../adapter/sheets/providers/google-sheets-api/errors.js";
|
|
28
27
|
import { columnLetters } from "../../../adapter/sheets/providers/google-sheets-api/model/valueNormalization.js";
|
|
28
|
+
import { GOOGLE_SHEETS_API_DEFAULTS, } from "../../../adapter/sheets/providers/google-sheets-api/constants.js";
|
|
29
29
|
import { PRESENCE_KINDS } from "../../../shared/state/index.js";
|
|
30
|
+
import { DEFAULT_EFFECT_LEASE_DURATION_MS, EFFECT_LEASE_PROVIDER_HEADROOM_MS, } from "@hikoutei/ikisaki";
|
|
30
31
|
import { SYNC_CONFLICT_PROJECTION_REGISTERED_RANGE, } from "../sheetsContract/conflictProjection.js";
|
|
31
32
|
import { SyncSheetsContractError } from "../sheetsContract/errors.js";
|
|
33
|
+
import { describeErrorForInternalLog, logHikouteiInternalEvent, } from "../../../shared/observability/internalLog.js";
|
|
34
|
+
import { HIKOUTEI_LOG_COMPONENTS, HIKOUTEI_LOG_EVENTS, HIKOUTEI_LOG_STABLE_CLASSES, HIKOUTEI_LOG_STABLE_CODES, } from "../../../shared/observability/logEvents.js";
|
|
32
35
|
import { createInternalSyncService, } from "./SyncServiceBootstrap.js";
|
|
33
|
-
import { positiveDecimalMillisecondsSchema, syncCredentialsSchema, } from "./configSchemas.js";
|
|
34
36
|
/** Env keys consumed by the sync auto-start bridge. */
|
|
35
37
|
export const SYNC_ENV_KEYS = {
|
|
36
38
|
/** Spreadsheet URL; absent means sync stays disabled (local-only). */
|
|
@@ -41,11 +43,46 @@ export const SYNC_ENV_KEYS = {
|
|
|
41
43
|
POLLING_INTERVAL_MS: "HIKOUTEI_SYNC_POLLING_INTERVAL_MS",
|
|
42
44
|
/** Optional metadata safety full-scan cadence in ms; defaults to 60 seconds. */
|
|
43
45
|
FULL_SCAN_INTERVAL_MS: "HIKOUTEI_SYNC_FULL_SCAN_INTERVAL_MS",
|
|
46
|
+
/**
|
|
47
|
+
* Optional request-start pacing in ms for the direct provider's ONE shared
|
|
48
|
+
* read+write limiter; absent uses the safe default (2,500 ms). Internal
|
|
49
|
+
* only — never part of the root public API.
|
|
50
|
+
*/
|
|
51
|
+
RATE_LIMIT_INTERVAL_MS: "HIKOUTEI_SYNC_RATE_LIMIT_INTERVAL_MS",
|
|
44
52
|
};
|
|
45
53
|
/** Default polling cadence applied when the interval env vars are absent. */
|
|
46
54
|
export const DEFAULT_SYNC_POLLING_INTERVAL_MS = 60_000;
|
|
47
55
|
/** Default safety full-scan cadence applied when the interval env vars are absent. */
|
|
48
56
|
export const DEFAULT_SYNC_FULL_SCAN_INTERVAL_MS = 60_000;
|
|
57
|
+
/** Lower bound for the sync request-start pacing env override (1 second). */
|
|
58
|
+
export const MIN_SYNC_RATE_LIMIT_INTERVAL_MS = 1_000;
|
|
59
|
+
/**
|
|
60
|
+
* Upper bound for the sync request-start pacing env override.
|
|
61
|
+
*
|
|
62
|
+
* A worst-case effect dispatch performs THREE sequential paced transport
|
|
63
|
+
* calls (two preflight/postcondition reads plus one batch write). The effect
|
|
64
|
+
* lease must still cover the whole sequence with the 30-second provider
|
|
65
|
+
* headroom, and a dispatch can wait up to one FULL interval for its first
|
|
66
|
+
* slot because the shared limiter may hold a prior reservation — admission
|
|
67
|
+
* is BOUNDED to one interval, so a request whose slot lies further out is
|
|
68
|
+
* refused before any SDK call (delivery-uncertain, requeued durably)
|
|
69
|
+
* instead of waiting past the lease. With the DEFAULT lease, write timeout,
|
|
70
|
+
* and read timeout the interval must satisfy
|
|
71
|
+
* `120s > 60s + I + 2 * max(10s, I) + 30s`, i.e. `I < 10s`. Below
|
|
72
|
+
* the 10 s read timeout the two read slots cost 2 x 10 s and the interval
|
|
73
|
+
* adds once, so the bound is `I < 120 - 60 - 2x10 - 30 = 10 s`; at or
|
|
74
|
+
* above the read timeout the interval dominates (`3I < 30s` has no
|
|
75
|
+
* solution at or above 10 s), so the below-read-slot branch is binding.
|
|
76
|
+
* The ceiling below is derived from those defaults (10,000 - 1 = 9,999 ms)
|
|
77
|
+
* and the service-level lease-headroom validation applies the same math to
|
|
78
|
+
* the ACTIVE lease and timeouts, so an override that could let pacing push
|
|
79
|
+
* a dispatch past the lease is rejected with a stable startup failure
|
|
80
|
+
* instead of risking lease expiry and duplicate remote delivery.
|
|
81
|
+
*/
|
|
82
|
+
export const MAX_SYNC_RATE_LIMIT_INTERVAL_MS = Math.max(MIN_SYNC_RATE_LIMIT_INTERVAL_MS, Math.floor(DEFAULT_EFFECT_LEASE_DURATION_MS -
|
|
83
|
+
GOOGLE_SHEETS_API_DEFAULTS.REQUEST_TIMEOUT_MS -
|
|
84
|
+
EFFECT_LEASE_PROVIDER_HEADROOM_MS -
|
|
85
|
+
2 * GOOGLE_SHEETS_API_DEFAULTS.READ_TIMEOUT_MS) - 1);
|
|
49
86
|
/** Required service-account fields validated before any remote contact. */
|
|
50
87
|
const REQUIRED_CREDENTIAL_FIELDS = ["type", "client_email", "private_key"];
|
|
51
88
|
const SPREADSHEET_PATH_SEGMENT = "d";
|
|
@@ -112,20 +149,42 @@ export async function createTypedSheetsWithSync(options) {
|
|
|
112
149
|
({ clientEmail } = await validateSyncCredentialsFile(env[SYNC_ENV_KEYS.CREDENTIALS_FILE]));
|
|
113
150
|
const pollingIntervalMs = parseIntervalEnv(env, SYNC_ENV_KEYS.POLLING_INTERVAL_MS, DEFAULT_SYNC_POLLING_INTERVAL_MS);
|
|
114
151
|
const pollingFullScanIntervalMs = parseIntervalEnv(env, SYNC_ENV_KEYS.FULL_SCAN_INTERVAL_MS, DEFAULT_SYNC_FULL_SCAN_INTERVAL_MS);
|
|
152
|
+
// The pacing env override applies ONLY to the real Google Sheets
|
|
153
|
+
// provider: it is resolved and validated only when no fake transport is
|
|
154
|
+
// injected, so local/fake suites stay immune to a misconfigured or
|
|
155
|
+
// invalid override in the host env.
|
|
156
|
+
const rateLimitIntervalMs = options.transport === undefined
|
|
157
|
+
? resolveSyncRateLimitIntervalMs(env)
|
|
158
|
+
: undefined;
|
|
115
159
|
const projections = buildSyncProjections(options.entities, spreadsheetId);
|
|
116
160
|
const service = await createInternalSyncService({
|
|
117
161
|
dbName: options.dbName,
|
|
118
162
|
entities: [...options.entities],
|
|
119
163
|
projections,
|
|
120
|
-
// Injected transports are a test-only affordance;
|
|
121
|
-
// stub-based suites wall-clock cheap
|
|
122
|
-
//
|
|
164
|
+
// Injected transports are a test-only affordance; ZERO pacing keeps
|
|
165
|
+
// stub-based suites wall-clock cheap AND immune to the bounded
|
|
166
|
+
// admission (interval 0 never waits and never refuses). The pacing env
|
|
167
|
+
// override applies ONLY to the real Google Sheets provider (no injected
|
|
168
|
+
// transport): a valid override is plumbed through, and the production
|
|
169
|
+
// path builds the real ADC-backed transport with the provider's safe
|
|
170
|
+
// default (2,500 ms) when the override is absent. Fake transports never
|
|
171
|
+
// consult HIKOUTEI_SYNC_RATE_LIMIT_INTERVAL_MS, so local/fake suites
|
|
172
|
+
// stay immune to a misconfigured or invalid override in the host env.
|
|
123
173
|
googleSheetsApi: options.transport === undefined
|
|
124
|
-
? {}
|
|
125
|
-
: {
|
|
174
|
+
? (rateLimitIntervalMs === undefined ? {} : { rateLimitIntervalMs })
|
|
175
|
+
: {
|
|
176
|
+
transport: options.transport,
|
|
177
|
+
rateLimitIntervalMs: 0,
|
|
178
|
+
},
|
|
126
179
|
pollingIntervalMs,
|
|
127
180
|
pollingFullScanIntervalMs,
|
|
128
181
|
});
|
|
182
|
+
logHikouteiInternalEvent({
|
|
183
|
+
event: HIKOUTEI_LOG_EVENTS.SYNC_AUTOSTART_STARTED,
|
|
184
|
+
level: "info",
|
|
185
|
+
component: HIKOUTEI_LOG_COMPONENTS.SYNC_AUTOSTART,
|
|
186
|
+
counts: { entities: options.entities.length },
|
|
187
|
+
});
|
|
129
188
|
return { kind: "sync", hikoutei: service.hikoutei, service };
|
|
130
189
|
}
|
|
131
190
|
catch (error) {
|
|
@@ -164,16 +223,15 @@ export async function validateSyncCredentialsFile(path) {
|
|
|
164
223
|
catch {
|
|
165
224
|
throw new HikouteiError(HIKOUTEI_ERROR_CODES.SYNC_CREDENTIALS_INVALID_JSON, `Credentials file is not valid JSON: ${path}`);
|
|
166
225
|
}
|
|
167
|
-
|
|
168
|
-
if (!parsedRecord.success) {
|
|
226
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
169
227
|
throw new HikouteiError(HIKOUTEI_ERROR_CODES.SYNC_CREDENTIALS_INVALID_JSON, `Credentials file is not valid JSON: ${path}`);
|
|
170
228
|
}
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
229
|
+
const record = parsed;
|
|
230
|
+
const missing = REQUIRED_CREDENTIAL_FIELDS.filter((field) => typeof record[field] !== "string" || record[field].trim() === "");
|
|
231
|
+
if (missing.length > 0) {
|
|
174
232
|
throw new HikouteiError(HIKOUTEI_ERROR_CODES.SYNC_CREDENTIALS_FIELD_MISSING, `Credentials file is missing required fields: ${missing.join(", ")}`);
|
|
175
233
|
}
|
|
176
|
-
return { clientEmail:
|
|
234
|
+
return { clientEmail: record.client_email };
|
|
177
235
|
}
|
|
178
236
|
/**
|
|
179
237
|
* Auto-generates the internal projection routes from public entity descriptors.
|
|
@@ -252,23 +310,132 @@ function parseIntervalEnv(env, key, fallback) {
|
|
|
252
310
|
const raw = env[key];
|
|
253
311
|
if (raw === undefined || raw.trim() === "")
|
|
254
312
|
return fallback;
|
|
255
|
-
|
|
256
|
-
|
|
313
|
+
// Decimal-only shape: Number() would also accept hex ("0x10"), exponent
|
|
314
|
+
// ("1e3"), signed ("-1"), and whitespace-padded (" 60000") forms that
|
|
315
|
+
// violate the positive-integer-ms contract, so the raw string must match
|
|
316
|
+
// before conversion.
|
|
317
|
+
if (!/^\d+$/.test(raw)) {
|
|
318
|
+
throw new HikouteiError(HIKOUTEI_ERROR_CODES.SYNC_STARTUP_FAILED, `Sync start failed: ${key} must be a positive integer (ms) — current value: ${raw}`);
|
|
319
|
+
}
|
|
320
|
+
const value = Number(raw);
|
|
321
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
257
322
|
throw new HikouteiError(HIKOUTEI_ERROR_CODES.SYNC_STARTUP_FAILED, `Sync start failed: ${key} must be a positive integer (ms) — current value: ${raw}`);
|
|
258
323
|
}
|
|
259
|
-
return value
|
|
324
|
+
return value;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Resolves the sync request-start pacing env override, or undefined.
|
|
328
|
+
*
|
|
329
|
+
* `HIKOUTEI_SYNC_RATE_LIMIT_INTERVAL_MS` is the internal override for the
|
|
330
|
+
* direct provider's ONE shared read+write request-start limiter. Absent or
|
|
331
|
+
* blank means the provider's safe default (2,500 ms) applies; a present
|
|
332
|
+
* value must be a plain decimal integer between 1,000 ms and
|
|
333
|
+
* `MAX_SYNC_RATE_LIMIT_INTERVAL_MS` (~10 s). The ceiling is the largest
|
|
334
|
+
* interval whose worst-case three-request paced dispatch still finishes
|
|
335
|
+
* inside the default effect lease (120 s) with the default write timeout
|
|
336
|
+
* (60 s), read timeout (10 s), and provider headroom (30 s): a dispatch
|
|
337
|
+
* can wait up to one full interval for its first slot because the shared
|
|
338
|
+
* limiter may hold a prior reservation, so the strict default-safe bound
|
|
339
|
+
* is `120s > 60s + I + 2 * max(10s, I) + 30s` and a larger interval could
|
|
340
|
+
* let pacing push the dispatch past the lease and risk duplicate remote
|
|
341
|
+
* delivery. Values outside those bounds (including hex,
|
|
342
|
+
* exponent, signed, padded, fractional, and non-numeric forms) fail closed
|
|
343
|
+
* with the stable startup code so a mistyped override can never silently
|
|
344
|
+
* drop pacing to a burst or outlive the effect lease. The key stays internal
|
|
345
|
+
* and is never part of the root public API.
|
|
346
|
+
*/
|
|
347
|
+
export function resolveSyncRateLimitIntervalMs(env) {
|
|
348
|
+
const raw = env[SYNC_ENV_KEYS.RATE_LIMIT_INTERVAL_MS];
|
|
349
|
+
if (raw === undefined || raw.trim() === "")
|
|
350
|
+
return undefined;
|
|
351
|
+
if (!/^\d+$/.test(raw)) {
|
|
352
|
+
throw new HikouteiError(HIKOUTEI_ERROR_CODES.SYNC_STARTUP_FAILED, `Sync start failed: ${SYNC_ENV_KEYS.RATE_LIMIT_INTERVAL_MS} must be an integer between ` +
|
|
353
|
+
`${MIN_SYNC_RATE_LIMIT_INTERVAL_MS} and ${MAX_SYNC_RATE_LIMIT_INTERVAL_MS} ms — current value: ${raw}`);
|
|
354
|
+
}
|
|
355
|
+
const value = Number(raw);
|
|
356
|
+
if (!Number.isSafeInteger(value) ||
|
|
357
|
+
value < MIN_SYNC_RATE_LIMIT_INTERVAL_MS ||
|
|
358
|
+
value > MAX_SYNC_RATE_LIMIT_INTERVAL_MS) {
|
|
359
|
+
throw new HikouteiError(HIKOUTEI_ERROR_CODES.SYNC_STARTUP_FAILED, `Sync start failed: ${SYNC_ENV_KEYS.RATE_LIMIT_INTERVAL_MS} must be an integer between ` +
|
|
360
|
+
`${MIN_SYNC_RATE_LIMIT_INTERVAL_MS} and ${MAX_SYNC_RATE_LIMIT_INTERVAL_MS} ms — current value: ${raw}`);
|
|
361
|
+
}
|
|
362
|
+
return value;
|
|
260
363
|
}
|
|
261
364
|
function raiseDiagnosed(diagnostic, failure) {
|
|
262
|
-
|
|
365
|
+
logHikouteiInternalEvent({
|
|
366
|
+
event: HIKOUTEI_LOG_EVENTS.SYNC_AUTOSTART_FAILED,
|
|
367
|
+
level: "error",
|
|
368
|
+
component: HIKOUTEI_LOG_COMPONENTS.SYNC_AUTOSTART,
|
|
369
|
+
...describeErrorForInternalLog(failure),
|
|
370
|
+
});
|
|
371
|
+
// EVERY diagnostic sink — injected or default — receives only the
|
|
372
|
+
// stable class/code summary. Full failure messages can embed the
|
|
373
|
+
// service-account email, spreadsheet ID, credential path, or raw
|
|
374
|
+
// provider text and must never reach a sink; the thrown HikouteiError
|
|
375
|
+
// always carries the full public message regardless of the sink.
|
|
376
|
+
try {
|
|
377
|
+
diagnostic("error", stableStartupDiagnostic(failure));
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
// Fail-open: a throwing diagnostic sink (injected or default) can
|
|
381
|
+
// never replace the classified startup failure. The original
|
|
382
|
+
// HikouteiError — with its stable class and machine-readable code —
|
|
383
|
+
// is preserved and rethrown unchanged, exactly as if the sink had
|
|
384
|
+
// succeeded.
|
|
385
|
+
}
|
|
263
386
|
throw failure;
|
|
264
387
|
}
|
|
388
|
+
/** Stable error-class allowlist backing sink diagnostics (see logEvents.ts). */
|
|
389
|
+
const STABLE_CLASS_ALLOWLIST = new Set(HIKOUTEI_LOG_STABLE_CLASSES);
|
|
390
|
+
/** Stable error-code allowlist backing sink diagnostics (see logEvents.ts). */
|
|
391
|
+
const STABLE_CODE_ALLOWLIST = new Set(HIKOUTEI_LOG_STABLE_CODES);
|
|
392
|
+
/**
|
|
393
|
+
* Stable redacted startup summary for diagnostic sinks: class and code
|
|
394
|
+
* only, each checked against the shared stable allowlists.
|
|
395
|
+
*
|
|
396
|
+
* `failure.name` and `failure.code` are runtime strings that could carry
|
|
397
|
+
* secret-like text (paths, emails, tokens) if an unexpected error shape
|
|
398
|
+
* ever reached the sink path, so unknown or malformed values collapse to
|
|
399
|
+
* the fixed `unknown` class/code. The full public message is carried by
|
|
400
|
+
* the thrown HikouteiError alone and never reaches any sink.
|
|
401
|
+
*/
|
|
402
|
+
export function stableStartupDiagnostic(failure) {
|
|
403
|
+
const described = describeErrorForInternalLog(failure);
|
|
404
|
+
const errorClass = STABLE_CLASS_ALLOWLIST.has(described.errorClass)
|
|
405
|
+
? described.errorClass
|
|
406
|
+
: "unknown";
|
|
407
|
+
const code = described.code !== undefined && STABLE_CODE_ALLOWLIST.has(described.code)
|
|
408
|
+
? described.code
|
|
409
|
+
: "unknown";
|
|
410
|
+
return `Hikoutei sync autostart failed (class=${errorClass}, code=${code})`;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Shape gate for the default sink's error line (defense in depth).
|
|
414
|
+
*
|
|
415
|
+
* The error path receives only the stable class/code summary built by
|
|
416
|
+
* `stableStartupDiagnostic`, but the gate re-validates the exact summary
|
|
417
|
+
* shape before printing: an unexpected raw message (which could embed a
|
|
418
|
+
* path, email, spreadsheet ID, or provider text) collapses to the
|
|
419
|
+
* level-only fallback and never reaches the console.
|
|
420
|
+
*/
|
|
421
|
+
const STABLE_SUMMARY_SHAPE = /^Hikoutei sync autostart failed \(class=(unknown|[A-Za-z][A-Za-z0-9]*), code=(unknown|[A-Za-z][A-Za-z0-9_]*)\)$/;
|
|
265
422
|
function defaultDiagnostic(level, message) {
|
|
266
423
|
if (level === "info") {
|
|
424
|
+
// The info path emits only the static local-mode notice.
|
|
267
425
|
console.info(message);
|
|
426
|
+
return;
|
|
268
427
|
}
|
|
269
|
-
|
|
428
|
+
// The error path emits the already-redacted stable class/code summary
|
|
429
|
+
// (never the full failure message — that stays on the thrown
|
|
430
|
+
// HikouteiError). The shape gate above keeps the defense in depth:
|
|
431
|
+
// anything that is not the exact stable summary shape falls back to the
|
|
432
|
+
// level-only notice.
|
|
433
|
+
if (STABLE_SUMMARY_SHAPE.test(message)) {
|
|
270
434
|
console.error(message);
|
|
271
435
|
}
|
|
436
|
+
else {
|
|
437
|
+
console.error(`[hikoutei] sync autostart failed (level=${level})`);
|
|
438
|
+
}
|
|
272
439
|
}
|
|
273
440
|
function isNodeErrorWithCode(error, code) {
|
|
274
441
|
return error !== null &&
|