hikoutei 0.3.4 → 0.3.6
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.ja.md +44 -21
- package/README.ko.md +43 -23
- package/README.md +49 -33
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmMappedTypedSheets.d.ts +3 -3
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmMappedTypedSheets.d.ts.map +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmMappedTypedSheets.js +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarEntityRuntime.d.ts +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarEntityRuntime.js +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarRuntime.js +3 -3
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarRuntime.js.map +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPolling.d.ts +3 -3
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPolling.d.ts.map +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPolling.js +15 -15
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPolling.js.map +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingFastPath.d.ts +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingFastPath.d.ts.map +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingFastPath.js +3 -3
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingFastPath.js.map +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingInspection.d.ts +5 -5
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingInspection.d.ts.map +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingInspection.js +5 -5
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingInspection.js.map +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingPersistence.d.ts +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingPersistence.js +5 -5
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingPersistence.js.map +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingSql.js +3 -3
- package/dist/adapter/persistence/providers/mikro-orm/observation/MikroOrmUserInputPollingSql.js.map +1 -1
- package/dist/adapter/sheets/providers/google-sheets-api/GoogleSheetsApiSyncProvider.d.ts +156 -0
- package/dist/adapter/sheets/providers/google-sheets-api/GoogleSheetsApiSyncProvider.d.ts.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/GoogleSheetsApiSyncProvider.js +1112 -0
- package/dist/adapter/sheets/providers/google-sheets-api/GoogleSheetsApiSyncProvider.js.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/constants.d.ts +87 -0
- package/dist/adapter/sheets/providers/google-sheets-api/constants.d.ts.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/constants.js +97 -0
- package/dist/adapter/sheets/providers/google-sheets-api/constants.js.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/errors.d.ts +53 -0
- package/dist/adapter/sheets/providers/google-sheets-api/errors.d.ts.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/errors.js +61 -0
- package/dist/adapter/sheets/providers/google-sheets-api/errors.js.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/index.d.ts +16 -0
- package/dist/adapter/sheets/providers/google-sheets-api/index.d.ts.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/index.js +13 -0
- package/dist/adapter/sheets/providers/google-sheets-api/index.js.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/batchBuilder.d.ts +83 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/batchBuilder.d.ts.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/batchBuilder.js +413 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/batchBuilder.js.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/observation.d.ts +86 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/observation.d.ts.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/observation.js +439 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/observation.js.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/planner.d.ts +115 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/planner.d.ts.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/planner.js +522 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/planner.js.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/postcondition.d.ts +17 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/postcondition.d.ts.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/postcondition.js +99 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/postcondition.js.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/preflight.d.ts +242 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/preflight.d.ts.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/preflight.js +707 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/preflight.js.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/sheetIdAllocator.d.ts +16 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/sheetIdAllocator.d.ts.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/sheetIdAllocator.js +29 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/sheetIdAllocator.js.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/tableRead.d.ts +26 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/tableRead.d.ts.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/tableRead.js +57 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/tableRead.js.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/valueNormalization.d.ts +102 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/valueNormalization.d.ts.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/valueNormalization.js +375 -0
- package/dist/adapter/sheets/providers/google-sheets-api/model/valueNormalization.js.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/transport/googleSheetsApiTransport.d.ts +168 -0
- package/dist/adapter/sheets/providers/google-sheets-api/transport/googleSheetsApiTransport.d.ts.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/transport/googleSheetsApiTransport.js +247 -0
- package/dist/adapter/sheets/providers/google-sheets-api/transport/googleSheetsApiTransport.js.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/transport/rateLimiter.d.ts +47 -0
- package/dist/adapter/sheets/providers/google-sheets-api/transport/rateLimiter.d.ts.map +1 -0
- package/dist/adapter/sheets/providers/google-sheets-api/transport/rateLimiter.js +60 -0
- package/dist/adapter/sheets/providers/google-sheets-api/transport/rateLimiter.js.map +1 -0
- package/dist/api/Hikoutei.d.ts +1 -1
- package/dist/api/Hikoutei.js +1 -1
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +2 -2
- package/dist/application/orm/mapping/contracts.d.ts +2 -2
- package/dist/application/orm/mapping/contracts.d.ts.map +1 -1
- package/dist/application/orm/mapping/definition.js +5 -5
- package/dist/application/orm/mapping/definition.js.map +1 -1
- package/dist/application/orm/mapping/projection.js +7 -7
- package/dist/application/orm/mapping/projection.js.map +1 -1
- package/dist/application/orm/persistence/flush/flushCoordinator.d.ts +3 -3
- package/dist/application/orm/persistence/flush/flushCoordinator.d.ts.map +1 -1
- package/dist/application/orm/persistence/flush/flushCoordinator.js +2 -2
- package/dist/application/orm/persistence/flush/flushCoordinator.js.map +1 -1
- package/dist/application/orm/persistence/projection/projectionEffects.d.ts +1 -1
- package/dist/application/orm/persistence/projection/projectionEffects.js +6 -6
- package/dist/application/orm/persistence/projection/projectionEffects.js.map +1 -1
- package/dist/application/orm/persistence/support/contracts.d.ts +2 -2
- package/dist/application/orm/persistence/support/contracts.d.ts.map +1 -1
- package/dist/application/sync/inbound/autoSystemConflictResolution.js +7 -7
- package/dist/application/sync/inbound/autoSystemConflictResolution.js.map +1 -1
- package/dist/application/sync/inbound/polling/SimpleSheetPolling.d.ts +3 -3
- package/dist/application/sync/inbound/polling/SimpleSheetPolling.d.ts.map +1 -1
- package/dist/application/sync/inbound/polling/SimpleSheetPolling.js +1 -1
- package/dist/application/sync/inbound/polling/SimpleSheetPolling.js.map +1 -1
- package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.d.ts +17 -1
- package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.d.ts.map +1 -1
- package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.js +27 -1
- package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.js.map +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectSupervisor.d.ts +2 -2
- package/dist/application/sync/outbound/effects/SyncEffectSupervisor.d.ts.map +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectSupervisor.js +26 -6
- package/dist/application/sync/outbound/effects/SyncEffectSupervisor.js.map +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorker.d.ts +24 -9
- package/dist/application/sync/outbound/effects/SyncEffectWorker.d.ts.map +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorker.js +128 -81
- package/dist/application/sync/outbound/effects/SyncEffectWorker.js.map +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.d.ts +31 -14
- package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.d.ts.map +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.js +32 -15
- package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.js.map +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.d.ts +13 -9
- package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.d.ts.map +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.js +53 -37
- package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.js.map +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerHelpers.js +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerHelpers.js.map +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.d.ts +29 -19
- package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.d.ts.map +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.js +59 -41
- package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.js.map +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.d.ts +4 -4
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.d.ts.map +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.js +3 -3
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.js.map +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.d.ts +3 -3
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.d.ts.map +1 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.js +42 -46
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.js.map +1 -1
- package/dist/application/sync/outbound/projection/ProjectionEffectFactory.d.ts +8 -8
- package/dist/application/sync/outbound/projection/ProjectionEffectFactory.d.ts.map +1 -1
- package/dist/application/sync/outbound/projection/ProjectionEffectFactory.js +15 -15
- package/dist/application/sync/outbound/projection/ProjectionEffectFactory.js.map +1 -1
- package/dist/application/sync/outbound/reconciliation/ReconciliationScanner.d.ts +3 -3
- package/dist/application/sync/outbound/reconciliation/ReconciliationScanner.d.ts.map +1 -1
- package/dist/application/sync/outbound/reconciliation/ReconciliationScanner.js +10 -10
- package/dist/application/sync/outbound/reconciliation/ReconciliationScanner.js.map +1 -1
- package/dist/application/sync/service/SyncPollingSupervisor.d.ts +1 -1
- package/dist/application/sync/service/SyncPollingSupervisor.js +1 -1
- package/dist/application/sync/service/SyncServiceBootstrap.d.ts +29 -19
- package/dist/application/sync/service/SyncServiceBootstrap.d.ts.map +1 -1
- package/dist/application/sync/service/SyncServiceBootstrap.js +103 -78
- package/dist/application/sync/service/SyncServiceBootstrap.js.map +1 -1
- package/dist/application/sync/service/errors.d.ts +1 -1
- package/dist/application/sync/service/errors.js +1 -1
- package/dist/application/sync/service/errors.js.map +1 -1
- package/dist/application/sync/sheets/conflictProjection.d.ts.map +1 -0
- package/dist/application/sync/sheets/conflictProjection.js.map +1 -0
- package/dist/application/sync/{gateway → sheets}/conflictProjectionRegistration.d.ts +1 -1
- package/dist/application/sync/sheets/conflictProjectionRegistration.d.ts.map +1 -0
- package/dist/application/sync/{gateway → sheets}/conflictProjectionRegistration.js +3 -3
- package/dist/application/sync/sheets/conflictProjectionRegistration.js.map +1 -0
- package/dist/application/sync/sheets/constants.d.ts +66 -0
- package/dist/application/sync/sheets/constants.d.ts.map +1 -0
- package/dist/application/sync/{gateway → sheets}/constants.js +15 -15
- package/dist/application/sync/sheets/constants.js.map +1 -0
- package/dist/application/sync/sheets/errors.d.ts +15 -0
- package/dist/application/sync/sheets/errors.d.ts.map +1 -0
- package/dist/application/sync/sheets/errors.js +16 -0
- package/dist/application/sync/sheets/errors.js.map +1 -0
- package/dist/application/sync/{gateway/coordinator/CoordinatedSyncGateway.d.ts → sheets/mutationCoordinator/CoordinatedSheetsProvider.d.ts} +29 -28
- package/dist/application/sync/sheets/mutationCoordinator/CoordinatedSheetsProvider.d.ts.map +1 -0
- package/dist/application/sync/{gateway/coordinator/CoordinatedSyncGateway.js → sheets/mutationCoordinator/CoordinatedSheetsProvider.js} +27 -26
- package/dist/application/sync/sheets/mutationCoordinator/CoordinatedSheetsProvider.js.map +1 -0
- package/dist/application/sync/{gateway/coordinator → sheets/mutationCoordinator}/asyncMutex.d.ts +2 -3
- package/dist/application/sync/sheets/mutationCoordinator/asyncMutex.d.ts.map +1 -0
- package/dist/application/sync/sheets/mutationCoordinator/asyncMutex.js.map +1 -0
- package/dist/application/sync/{gateway/coordinator/coordinatorTelemetry.d.ts → sheets/mutationCoordinator/laneTelemetry.d.ts} +4 -4
- package/dist/application/sync/sheets/mutationCoordinator/laneTelemetry.d.ts.map +1 -0
- package/dist/application/sync/{gateway/coordinator/coordinatorTelemetry.js → sheets/mutationCoordinator/laneTelemetry.js} +5 -5
- package/dist/application/sync/sheets/mutationCoordinator/laneTelemetry.js.map +1 -0
- package/dist/application/sync/{gateway/SyncGatewayBootstrap.d.ts → sheets/sheetsProvisioning.d.ts} +14 -14
- package/dist/application/sync/sheets/sheetsProvisioning.d.ts.map +1 -0
- package/dist/application/sync/sheets/sheetsProvisioning.js +95 -0
- package/dist/application/sync/sheets/sheetsProvisioning.js.map +1 -0
- package/dist/application/sync/{gateway/syncGateway.d.ts → sheets/syncSheets.d.ts} +61 -117
- package/dist/application/sync/sheets/syncSheets.d.ts.map +1 -0
- package/dist/application/sync/sheets/syncSheets.js +144 -0
- package/dist/application/sync/sheets/syncSheets.js.map +1 -0
- package/dist/application/sync/{gateway/transportClassification.d.ts → sheets/transportOutcome.d.ts} +23 -22
- package/dist/application/sync/sheets/transportOutcome.d.ts.map +1 -0
- package/dist/application/sync/sheets/transportOutcome.js +97 -0
- package/dist/application/sync/sheets/transportOutcome.js.map +1 -0
- package/dist/application/sync/sheets/validation.d.ts +22 -0
- package/dist/application/sync/sheets/validation.d.ts.map +1 -0
- package/dist/application/sync/sheets/validation.js +79 -0
- package/dist/application/sync/sheets/validation.js.map +1 -0
- package/dist/application/sync/telemetry/syncTiming.d.ts +7 -7
- package/dist/application/sync/telemetry/syncTiming.d.ts.map +1 -1
- package/dist/application/sync/telemetry/syncTiming.js +2 -2
- package/dist/application/sync/telemetry/syncTiming.js.map +1 -1
- package/dist/domain/evaluate/contracts.d.ts +1 -1
- package/dist/domain/evaluate/contracts.js +1 -1
- package/dist/domain/model/constants.d.ts +1 -1
- package/dist/domain/model/constants.d.ts.map +1 -1
- package/dist/domain/model/types.d.ts +1 -1
- package/dist/domain/model/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/storage/index.d.ts +1 -1
- package/dist/infrastructure/storage/index.d.ts.map +1 -1
- package/dist/infrastructure/storage/index.js +1 -1
- package/dist/infrastructure/storage/index.js.map +1 -1
- package/dist/infrastructure/storage/state/observation/observationAudit.d.ts +1 -1
- package/dist/infrastructure/storage/state/observation/observationAudit.js +1 -1
- package/dist/infrastructure/storage/state/observation/observationTypes.d.ts +3 -3
- package/dist/infrastructure/storage/state/observation/observationTypes.d.ts.map +1 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutbox.d.ts +13 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutbox.d.ts.map +1 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutbox.js +23 -2
- package/dist/infrastructure/storage/sync/outbound/effectOutbox.js.map +1 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.d.ts +9 -3
- package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.d.ts.map +1 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.js +7 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.js.map +1 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.d.ts +11 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.d.ts.map +1 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.js +44 -3
- package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.js.map +1 -1
- package/dist/infrastructure/storage/sync/shared/syncRegistry.d.ts +4 -4
- package/dist/infrastructure/storage/sync/shared/syncRegistry.d.ts.map +1 -1
- package/dist/infrastructure/storage/sync/shared/syncRegistry.js +4 -4
- package/dist/infrastructure/storage/sync/shared/syncRegistry.js.map +1 -1
- package/dist/shared/encoding/types.d.ts +1 -1
- package/docs/architecture.md +52 -23
- package/docs/ci.md +31 -23
- package/docs/code-guidelines.md +10 -18
- package/docs/development.md +37 -11
- package/docs/gateway-removal-inventory.md +147 -0
- package/docs/quick-start.md +36 -26
- package/docs/sync-bulk-write-benchmark.md +561 -0
- package/docs/sync-observability.md +38 -45
- package/docs/write-and-synchronization-flow.md +56 -39
- package/package.json +5 -4
- package/apps-script/gateway/Code.gs +0 -447
- package/dist/adapter/sheets/providers/apps-script-gateway/errors.d.ts +0 -43
- package/dist/adapter/sheets/providers/apps-script-gateway/errors.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/errors.js +0 -52
- package/dist/adapter/sheets/providers/apps-script-gateway/errors.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/index.d.ts +0 -24
- package/dist/adapter/sheets/providers/apps-script-gateway/index.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/index.js +0 -13
- package/dist/adapter/sheets/providers/apps-script-gateway/index.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/effect/effectOperation.d.ts +0 -44
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/effect/effectOperation.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/effect/effectOperation.js +0 -181
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/effect/effectOperation.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/effect/effectOperationScript.d.ts +0 -3
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/effect/effectOperationScript.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/effect/effectOperationScript.js +0 -810
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/effect/effectOperationScript.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/observation/observationOperation.d.ts +0 -31
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/observation/observationOperation.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/observation/observationOperation.js +0 -560
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/observation/observationOperation.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/read/tableReadOperation.d.ts +0 -12
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/read/tableReadOperation.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/read/tableReadOperation.js +0 -166
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/read/tableReadOperation.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/shared/appsScriptStableCodecSource.d.ts +0 -8
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/shared/appsScriptStableCodecSource.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/shared/appsScriptStableCodecSource.js +0 -122
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/shared/appsScriptStableCodecSource.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/batchAppendOperation.d.ts +0 -31
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/batchAppendOperation.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/batchAppendOperation.js +0 -567
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/batchAppendOperation.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/fastAppendOperation.d.ts +0 -19
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/fastAppendOperation.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/fastAppendOperation.js +0 -37
- package/dist/adapter/sheets/providers/apps-script-gateway/operations/write/fastAppendOperation.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/codeGsProtocol.d.ts +0 -51
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/codeGsProtocol.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/codeGsProtocol.js +0 -60
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/codeGsProtocol.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/constants.d.ts +0 -44
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/constants.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/constants.js +0 -40
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/constants.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/syncProtocol.d.ts +0 -12
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/syncProtocol.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/syncProtocol.js +0 -41
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/syncProtocol.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/timing.d.ts +0 -5
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/timing.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/timing.js +0 -53
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/timing.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/types.d.ts +0 -4
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/types.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/types.js +0 -2
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/types.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/validation.d.ts +0 -11
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/validation.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/validation.js +0 -36
- package/dist/adapter/sheets/providers/apps-script-gateway/protocol/validation.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/transport/operationClient.d.ts +0 -68
- package/dist/adapter/sheets/providers/apps-script-gateway/transport/operationClient.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/transport/operationClient.js +0 -273
- package/dist/adapter/sheets/providers/apps-script-gateway/transport/operationClient.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/transport/operationSyncGateway.d.ts +0 -73
- package/dist/adapter/sheets/providers/apps-script-gateway/transport/operationSyncGateway.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/transport/operationSyncGateway.js +0 -363
- package/dist/adapter/sheets/providers/apps-script-gateway/transport/operationSyncGateway.js.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/validation.d.ts +0 -3
- package/dist/adapter/sheets/providers/apps-script-gateway/validation.d.ts.map +0 -1
- package/dist/adapter/sheets/providers/apps-script-gateway/validation.js +0 -10
- package/dist/adapter/sheets/providers/apps-script-gateway/validation.js.map +0 -1
- package/dist/application/sync/gateway/SyncGatewayBootstrap.d.ts.map +0 -1
- package/dist/application/sync/gateway/SyncGatewayBootstrap.js +0 -95
- package/dist/application/sync/gateway/SyncGatewayBootstrap.js.map +0 -1
- package/dist/application/sync/gateway/conflictProjection.d.ts.map +0 -1
- package/dist/application/sync/gateway/conflictProjection.js.map +0 -1
- package/dist/application/sync/gateway/conflictProjectionRegistration.d.ts.map +0 -1
- package/dist/application/sync/gateway/conflictProjectionRegistration.js.map +0 -1
- package/dist/application/sync/gateway/constants.d.ts +0 -66
- package/dist/application/sync/gateway/constants.d.ts.map +0 -1
- package/dist/application/sync/gateway/constants.js.map +0 -1
- package/dist/application/sync/gateway/coordinator/CoordinatedSyncGateway.d.ts.map +0 -1
- package/dist/application/sync/gateway/coordinator/CoordinatedSyncGateway.js.map +0 -1
- package/dist/application/sync/gateway/coordinator/asyncMutex.d.ts.map +0 -1
- package/dist/application/sync/gateway/coordinator/asyncMutex.js.map +0 -1
- package/dist/application/sync/gateway/coordinator/coordinatorTelemetry.d.ts.map +0 -1
- package/dist/application/sync/gateway/coordinator/coordinatorTelemetry.js.map +0 -1
- package/dist/application/sync/gateway/errors.d.ts +0 -15
- package/dist/application/sync/gateway/errors.d.ts.map +0 -1
- package/dist/application/sync/gateway/errors.js +0 -16
- package/dist/application/sync/gateway/errors.js.map +0 -1
- package/dist/application/sync/gateway/syncGateway.d.ts.map +0 -1
- package/dist/application/sync/gateway/syncGateway.js +0 -191
- package/dist/application/sync/gateway/syncGateway.js.map +0 -1
- package/dist/application/sync/gateway/transportClassification.d.ts.map +0 -1
- package/dist/application/sync/gateway/transportClassification.js +0 -133
- package/dist/application/sync/gateway/transportClassification.js.map +0 -1
- package/dist/application/sync/gateway/validation.d.ts +0 -22
- package/dist/application/sync/gateway/validation.d.ts.map +0 -1
- package/dist/application/sync/gateway/validation.js +0 -79
- package/dist/application/sync/gateway/validation.js.map +0 -1
- /package/dist/application/sync/{gateway → sheets}/conflictProjection.d.ts +0 -0
- /package/dist/application/sync/{gateway → sheets}/conflictProjection.js +0 -0
- /package/dist/application/sync/{gateway/coordinator → sheets/mutationCoordinator}/asyncMutex.js +0 -0
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full sync provider over the Google Sheets REST API.
|
|
3
|
+
*
|
|
4
|
+
* Implements every provider capability the sync runtime needs with ONE
|
|
5
|
+
* provider instance (one transport, one read limiter, one write limiter, one
|
|
6
|
+
* telemetry sink): the outbound effect worker (fast append, applyEffects,
|
|
7
|
+
* postcondition recovery), projection provisioning, values-only table reads,
|
|
8
|
+
* row-anchor assignment, and full metadata snapshots. The service-account
|
|
9
|
+
* bootstrap mode uses this provider exclusively and needs no Apps Script.
|
|
10
|
+
*
|
|
11
|
+
* Delivery semantics are ported from the Apps Script operations: bulk
|
|
12
|
+
* preflight with fail-closed validation, receipt replay/idempotency,
|
|
13
|
+
* visible/candidate/repair compare-and-set guards, full-row deletion guards,
|
|
14
|
+
* one atomic `spreadsheets.batchUpdate` per target+receipt batch,
|
|
15
|
+
* response-loss recovery through receipt-backed postcondition reads,
|
|
16
|
+
* exact-match provisioning, and snapshot wire shapes that are byte-compatible
|
|
17
|
+
* with the Apps Script observation source. The provider never exposes
|
|
18
|
+
* credential material or raw Google SDK responses; telemetry carries only
|
|
19
|
+
* operation names, counts, durations, and stable codes.
|
|
20
|
+
*
|
|
21
|
+
* Request pacing is per transport call: every `getSpreadsheet` (preflight
|
|
22
|
+
* enumeration, preflight data, observation reads, post-reads, table reads,
|
|
23
|
+
* provisioning reads) and every `batchUpdate` acquires its class limiter and
|
|
24
|
+
* emits one redacted telemetry event.
|
|
25
|
+
*/
|
|
26
|
+
import { computeSyncVisibleHash, } from "../../../../application/sync/sheets/syncSheets.js";
|
|
27
|
+
import { SYNC_SHEETS_ERROR_CODES, SyncSheetsContractError, } from "../../../../application/sync/sheets/errors.js";
|
|
28
|
+
import { requireSyncSnapshotReadMode, } from "../../../../application/sync/sheets/validation.js";
|
|
29
|
+
import { SYNC_FAST_APPEND_STATUSES, SYNC_POSTCONDITION_MODES, SYNC_PROJECTIONS, SYNC_SNAPSHOT_READ_MODES, } from "../../../../application/sync/sheets/constants.js";
|
|
30
|
+
import { classifyTransportOutcome, } from "../../../../application/sync/sheets/transportOutcome.js";
|
|
31
|
+
import { presentValue, absentValue } from "../../../../shared/state/index.js";
|
|
32
|
+
import { isNormalizedCell } from "../../../../shared/encoding/index.js";
|
|
33
|
+
import { GOOGLE_SHEETS_API_DEFAULTS, GOOGLE_SHEETS_API_EFFECT_REASONS, GOOGLE_SHEETS_API_ANCHOR_KEY, } from "./constants.js";
|
|
34
|
+
import { invalidProviderRequest, invalidProviderState } from "./errors.js";
|
|
35
|
+
import { identityFromNormalizedCell } from "./model/valueNormalization.js";
|
|
36
|
+
import { GoogleSheetsApiHttpTransport, } from "./transport/googleSheetsApiTransport.js";
|
|
37
|
+
import { RequestStartLimiter } from "./transport/rateLimiter.js";
|
|
38
|
+
import { enumerateSheetProperties, readPreflightData, GOOGLE_SHEETS_API_PROVISION_ENUMERATION_FIELDS, GOOGLE_SHEETS_API_PROVISION_FIELDS, GOOGLE_SHEETS_API_VALUES_FIELDS, GOOGLE_SHEETS_API_OBSERVATION_FIELDS, GOOGLE_SHEETS_API_LIGHTWEIGHT_OBSERVATION_FIELDS, parseSpreadsheetDocument, gridHeaderCells, } from "./model/preflight.js";
|
|
39
|
+
import { planEffectBatch, encodeOutcomeResult, encodeSchemaErrorResult, withDeferredPostcondition, currentHash, } from "./model/planner.js";
|
|
40
|
+
import { buildApplyBatchRequests, buildAppendBatchRequests, resolveApplyBatchBudget, resolveAppendBudget, } from "./model/batchBuilder.js";
|
|
41
|
+
import { classifyPostcondition } from "./model/postcondition.js";
|
|
42
|
+
import { buildSnapshotFromTab, planRowAnchors, readTabGrids, } from "./model/observation.js";
|
|
43
|
+
import { buildTableRowsFromGrid } from "./model/tableRead.js";
|
|
44
|
+
import { allocateSheetId } from "./model/sheetIdAllocator.js";
|
|
45
|
+
import { columnLetters, parseRegisteredRange, quoteA1SheetName, } from "./model/valueNormalization.js";
|
|
46
|
+
/**
|
|
47
|
+
* Full sync provider over the Sheets REST API: outbound effects, provisioning,
|
|
48
|
+
* table reads, anchors, and snapshots behind the shared provider contracts.
|
|
49
|
+
*
|
|
50
|
+
* All reads share one read limiter; all writes share one write limiter; the
|
|
51
|
+
* worker-level append throttle stays enabled on top in service mode via the
|
|
52
|
+
* bootstrap's bulk worker options. Provisioning runs at startup before the
|
|
53
|
+
* worker, and observation anchors are the only metadata mutations outside
|
|
54
|
+
* effect batches.
|
|
55
|
+
*/
|
|
56
|
+
export class GoogleSheetsApiSyncProvider {
|
|
57
|
+
spreadsheetId;
|
|
58
|
+
definitions;
|
|
59
|
+
transport;
|
|
60
|
+
transportTimeoutMs;
|
|
61
|
+
readTimeoutMs;
|
|
62
|
+
maxBatchBytes;
|
|
63
|
+
readLimiter;
|
|
64
|
+
writeLimiter;
|
|
65
|
+
now;
|
|
66
|
+
onRequest;
|
|
67
|
+
constructor(options) {
|
|
68
|
+
if (options.definitions.length === 0) {
|
|
69
|
+
throw new SyncSheetsContractError(SYNC_SHEETS_ERROR_CODES.INVALID_PROVISIONING_DEFINITIONS, "Google Sheets API sync provider requires a projection definition");
|
|
70
|
+
}
|
|
71
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? GOOGLE_SHEETS_API_DEFAULTS.REQUEST_TIMEOUT_MS;
|
|
72
|
+
if (!Number.isSafeInteger(requestTimeoutMs) ||
|
|
73
|
+
requestTimeoutMs < GOOGLE_SHEETS_API_DEFAULTS.MIN_REQUEST_TIMEOUT_MS ||
|
|
74
|
+
requestTimeoutMs > GOOGLE_SHEETS_API_DEFAULTS.MAX_REQUEST_TIMEOUT_MS) {
|
|
75
|
+
invalidProviderRequest("Google Sheets API sync provider", "requestTimeoutMs must be between 1 second and 120 seconds");
|
|
76
|
+
}
|
|
77
|
+
const readTimeoutMs = options.readTimeoutMs ?? GOOGLE_SHEETS_API_DEFAULTS.READ_TIMEOUT_MS;
|
|
78
|
+
if (!Number.isSafeInteger(readTimeoutMs) ||
|
|
79
|
+
readTimeoutMs < GOOGLE_SHEETS_API_DEFAULTS.MIN_REQUEST_TIMEOUT_MS ||
|
|
80
|
+
readTimeoutMs > GOOGLE_SHEETS_API_DEFAULTS.MAX_READ_TIMEOUT_MS) {
|
|
81
|
+
invalidProviderRequest("Google Sheets API sync provider", "readTimeoutMs must be between 1 second and 60 seconds");
|
|
82
|
+
}
|
|
83
|
+
for (const definition of options.definitions) {
|
|
84
|
+
if (definition.sheet.spreadsheetId !== options.spreadsheetId) {
|
|
85
|
+
throw new SyncSheetsContractError(SYNC_SHEETS_ERROR_CODES.INVALID_PROVISIONING_DEFINITIONS, "Google Sheets API sync provider definitions must target one spreadsheet");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const maxBatchBytes = options.maxBatchBytes ?? GOOGLE_SHEETS_API_DEFAULTS.MAX_BATCH_REQUEST_BYTES;
|
|
89
|
+
if (!Number.isSafeInteger(maxBatchBytes) || maxBatchBytes < 1) {
|
|
90
|
+
invalidProviderRequest("Google Sheets API sync provider", "maxBatchBytes is invalid");
|
|
91
|
+
}
|
|
92
|
+
const intervalMs = options.rateLimitIntervalMs ?? GOOGLE_SHEETS_API_DEFAULTS.REQUEST_START_INTERVAL_MS;
|
|
93
|
+
this.spreadsheetId = options.spreadsheetId;
|
|
94
|
+
this.definitions = options.definitions;
|
|
95
|
+
this.transport = options.transport ?? new GoogleSheetsApiHttpTransport({ requestTimeoutMs });
|
|
96
|
+
this.transportTimeoutMs = requestTimeoutMs;
|
|
97
|
+
this.readTimeoutMs = readTimeoutMs;
|
|
98
|
+
this.maxBatchBytes = maxBatchBytes;
|
|
99
|
+
this.now = options.now ?? Date.now;
|
|
100
|
+
this.readLimiter = new RequestStartLimiter({
|
|
101
|
+
intervalMs,
|
|
102
|
+
now: this.now,
|
|
103
|
+
...(options.sleep === undefined ? {} : { sleep: options.sleep }),
|
|
104
|
+
});
|
|
105
|
+
this.writeLimiter = new RequestStartLimiter({
|
|
106
|
+
intervalMs,
|
|
107
|
+
now: this.now,
|
|
108
|
+
...(options.sleep === undefined ? {} : { sleep: options.sleep }),
|
|
109
|
+
});
|
|
110
|
+
this.onRequest = options.onRequest;
|
|
111
|
+
}
|
|
112
|
+
/** Exposes the configured outbound timeout (used by lease-headroom checks). */
|
|
113
|
+
get timeoutMs() {
|
|
114
|
+
return this.transportTimeoutMs;
|
|
115
|
+
}
|
|
116
|
+
// -------------------------------------------------------------------------
|
|
117
|
+
// Outbound effect worker (fast append, applyEffects, recovery)
|
|
118
|
+
// -------------------------------------------------------------------------
|
|
119
|
+
/** Appends rows through one idempotent, atomic target+receipt batch. */
|
|
120
|
+
async fastAppendRows(request) {
|
|
121
|
+
const definition = this.definitionForPhysicalSheet(request.physicalSheetId);
|
|
122
|
+
validateRoute(request, definition);
|
|
123
|
+
const routeOptions = effectRouteOptions(definition);
|
|
124
|
+
if (routeOptions.identityField.kind !== "present") {
|
|
125
|
+
// The fast path never materializes anchor metadata, so a route without
|
|
126
|
+
// a registered identity cannot locate or guard its rows on replay.
|
|
127
|
+
throw new SyncSheetsContractError(SYNC_SHEETS_ERROR_CODES.INVALID_EFFECT_PAYLOAD, "fast append requires a registered identityField for route " + request.physicalSheetId);
|
|
128
|
+
}
|
|
129
|
+
if (request.rows.length === 0) {
|
|
130
|
+
invalidProviderRequest("fast append", "rows must not be empty");
|
|
131
|
+
}
|
|
132
|
+
const bounded = request.rows.slice(0, GOOGLE_SHEETS_API_DEFAULTS.MAX_APPEND_ROWS_PER_REQUEST);
|
|
133
|
+
validateAppendRows(bounded);
|
|
134
|
+
const context = await this.readPreflight(request, definition, routeOptions);
|
|
135
|
+
validateAppendRowsAgainstHeaders(bounded, context.headers);
|
|
136
|
+
const identityField = routeOptions.identityField.value;
|
|
137
|
+
// Replay rows are recognized through the receipt tab and verified against
|
|
138
|
+
// the visible identity; pending rows go through the identity preflight.
|
|
139
|
+
const pending = [];
|
|
140
|
+
const resultsById = new Map();
|
|
141
|
+
const pendingReceipts = [];
|
|
142
|
+
for (const row of bounded) {
|
|
143
|
+
// The worker always supplies the outbox payload hash; fail closed when
|
|
144
|
+
// it is absent instead of silently falling back to the effect ID.
|
|
145
|
+
const payloadHash = row.payloadHash;
|
|
146
|
+
if (payloadHash === undefined || payloadHash.length === 0) {
|
|
147
|
+
invalidProviderRequest("fast append", `payloadHash is required for effectId: ${row.effectId}`);
|
|
148
|
+
}
|
|
149
|
+
const existing = context.receipts.get(row.effectId);
|
|
150
|
+
if (existing !== undefined) {
|
|
151
|
+
if (existing.payloadHash !== payloadHash) {
|
|
152
|
+
invalidProviderRequest("fast append", `effect ID cannot be reused with another payload: ${row.effectId}`);
|
|
153
|
+
}
|
|
154
|
+
const identity = appendIdentity(row, identityField);
|
|
155
|
+
const existingRow = findRowByIdentity(context, identity);
|
|
156
|
+
if (existingRow === undefined) {
|
|
157
|
+
invalidProviderRequest("fast append", `receipt postcondition row is unavailable for effectId: ${row.effectId}`);
|
|
158
|
+
}
|
|
159
|
+
if (computeSyncVisibleHash(existingRow.cells) !== existing.visibleHash) {
|
|
160
|
+
invalidProviderRequest("fast append", `receipt postcondition changed for effectId: ${row.effectId}`);
|
|
161
|
+
}
|
|
162
|
+
resultsById.set(row.effectId, {
|
|
163
|
+
effectId: row.effectId,
|
|
164
|
+
status: SYNC_FAST_APPEND_STATUSES.APPLIED,
|
|
165
|
+
visibleHash: existing.visibleHash,
|
|
166
|
+
visibleRevision: existing.visibleRevision,
|
|
167
|
+
});
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
pending.push(row);
|
|
171
|
+
pendingReceipts.push(makeAppendReceipt(row.effectId, payloadHash, computeSyncVisibleHash(row.fields)));
|
|
172
|
+
}
|
|
173
|
+
// Mirror the built-in append identity preflight: the registered identity
|
|
174
|
+
// must exist and be unique across the sheet and the pending batch; replay
|
|
175
|
+
// entries are exempt exactly like the real provider.
|
|
176
|
+
let deferredSuffix = false;
|
|
177
|
+
if (pending.length > 0) {
|
|
178
|
+
assertAppendIdentityAvailability(context, identityField, pending);
|
|
179
|
+
const pendingRows = pending.map((row, index) => toAppendWorkingRow(row, context.nextAppendRow + index));
|
|
180
|
+
const updatedAt = new Date(this.now()).toISOString();
|
|
181
|
+
const resolution = resolveAppendBudget(pendingRows, (count) => buildAppendBatchRequests(context, pendingRows.slice(0, count), pendingReceipts.slice(0, count), { updatedAt }), this.maxBatchBytes);
|
|
182
|
+
deferredSuffix = resolution.includeCount < pending.length;
|
|
183
|
+
if (resolution.includeCount > 0) {
|
|
184
|
+
const batch = buildAppendBatchRequests(context, pendingRows.slice(0, resolution.includeCount), pendingReceipts.slice(0, resolution.includeCount), { updatedAt });
|
|
185
|
+
const response = await this.runWrite(() => this.transport.batchUpdate({
|
|
186
|
+
spreadsheetId: this.spreadsheetId,
|
|
187
|
+
requests: batch.requests,
|
|
188
|
+
}));
|
|
189
|
+
requireValidBatchUpdateReply(response, batch.requests.length);
|
|
190
|
+
pendingReceipts.slice(0, resolution.includeCount).forEach((receipt) => {
|
|
191
|
+
resultsById.set(receipt.effectId, {
|
|
192
|
+
effectId: receipt.effectId,
|
|
193
|
+
status: SYNC_FAST_APPEND_STATUSES.APPLIED,
|
|
194
|
+
visibleHash: receipt.visibleHash,
|
|
195
|
+
visibleRevision: receipt.visibleRevision,
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// A byte-budget deferral commits only a prefix of the pending rows, so
|
|
201
|
+
// results cover exactly the processed rows: receipt-matched replays plus
|
|
202
|
+
// the included prefix. Rows beyond the included prefix are intentionally
|
|
203
|
+
// absent from results; the worker releases them (releaseUnprocessedEffect)
|
|
204
|
+
// for the next pass when hasMore is true.
|
|
205
|
+
const results = [];
|
|
206
|
+
for (const row of bounded) {
|
|
207
|
+
const result = resultsById.get(row.effectId);
|
|
208
|
+
if (result === undefined)
|
|
209
|
+
continue;
|
|
210
|
+
results.push(result);
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
results,
|
|
214
|
+
hasMore: bounded.length < request.rows.length || deferredSuffix,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/** Applies regular update/delete/create effects through one atomic batch. */
|
|
218
|
+
async applyEffects(request) {
|
|
219
|
+
const definition = this.definitionForPhysicalSheet(request.physicalSheetId);
|
|
220
|
+
validateRoute(request, definition);
|
|
221
|
+
if (request.effects.length === 0) {
|
|
222
|
+
invalidProviderRequest("apply effects", "effects must not be empty");
|
|
223
|
+
}
|
|
224
|
+
const bounded = request.effects.slice(0, GOOGLE_SHEETS_API_DEFAULTS.MAX_EFFECTS_PER_REQUEST);
|
|
225
|
+
const postconditionMode = request.postconditionMode ?? SYNC_POSTCONDITION_MODES.INLINE;
|
|
226
|
+
if (postconditionMode !== SYNC_POSTCONDITION_MODES.INLINE &&
|
|
227
|
+
postconditionMode !== SYNC_POSTCONDITION_MODES.DEFERRED) {
|
|
228
|
+
invalidProviderRequest("apply effects", "postconditionMode must be inline or deferred");
|
|
229
|
+
}
|
|
230
|
+
const routeOptions = effectRouteOptions(definition);
|
|
231
|
+
const context = await this.readPreflight(request, definition, routeOptions);
|
|
232
|
+
const plans = planEffectBatch({ ...request, effects: bounded }, context);
|
|
233
|
+
const includeReceipts = postconditionMode === SYNC_POSTCONDITION_MODES.DEFERRED;
|
|
234
|
+
const updatedAt = new Date(this.now()).toISOString();
|
|
235
|
+
const resolution = resolveApplyBatchBudget(context, plans, {
|
|
236
|
+
maxBatchBytes: this.maxBatchBytes,
|
|
237
|
+
includeReceipts,
|
|
238
|
+
updatedAt,
|
|
239
|
+
});
|
|
240
|
+
const schemaErrorIndices = new Set(resolution.schemaErrorIndices);
|
|
241
|
+
// Schema-error effects sit BEFORE the included run; the batch only carries
|
|
242
|
+
// the plans after them, up to the resolved include count. The included
|
|
243
|
+
// effects are re-planned so appended rows start at the sheet's first free
|
|
244
|
+
// row (the full-plan row numbers would leave blank gaps for excluded
|
|
245
|
+
// effects). The planner is deterministic over the unchanged context, so
|
|
246
|
+
// outcomes and receipts are identical to the budget-resolution plans.
|
|
247
|
+
const includedStart = resolution.schemaErrorIndices.length;
|
|
248
|
+
const includedEffects = bounded.slice(includedStart, resolution.includeCount);
|
|
249
|
+
const included = planEffectBatch({ ...request, effects: includedEffects }, context);
|
|
250
|
+
if (included.length > 0) {
|
|
251
|
+
const batch = buildApplyBatchRequests(context, included, { updatedAt, includeReceipts });
|
|
252
|
+
// Rejected plans (guard/schema/repair outcomes) contribute no requests;
|
|
253
|
+
// never send an empty batchUpdate for an all-rejected prefix.
|
|
254
|
+
if (batch.requests.length > 0) {
|
|
255
|
+
const response = await this.runWrite(() => this.transport.batchUpdate({
|
|
256
|
+
spreadsheetId: this.spreadsheetId,
|
|
257
|
+
requests: batch.requests,
|
|
258
|
+
}));
|
|
259
|
+
requireValidBatchUpdateReply(response, batch.requests.length);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Inline verification reads the written rows back and demotes any hash
|
|
263
|
+
// mismatch to retryable_error, mirroring the Apps Script inline path. The
|
|
264
|
+
// worker always uses deferred mode, where the atomic batch already carries
|
|
265
|
+
// target mutations and receipts together.
|
|
266
|
+
const verified = new Set();
|
|
267
|
+
if (postconditionMode === SYNC_POSTCONDITION_MODES.INLINE && included.length > 0) {
|
|
268
|
+
const verifyContext = await this.readPreflight(request, definition, routeOptions);
|
|
269
|
+
included.forEach((plan, index) => {
|
|
270
|
+
if (!plan.verify || plan.mutation === undefined || plan.mutation.kind === "delete")
|
|
271
|
+
return;
|
|
272
|
+
const row = findProbeRowInContext(verifyContext, plan);
|
|
273
|
+
const current = row === undefined ? undefined : currentHash(row, plan.outcome.effect.payload.fields);
|
|
274
|
+
if (current === plan.outcome.effect.payload.targetVisibleHash) {
|
|
275
|
+
verified.add(index);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
const verifyReceipts = [];
|
|
279
|
+
included.forEach((plan, index) => {
|
|
280
|
+
if (plan.receipt === undefined)
|
|
281
|
+
return;
|
|
282
|
+
// Replay receipts are already stored in the sheet; never rewrite them.
|
|
283
|
+
if (context.receipts.has(plan.receipt.effectId))
|
|
284
|
+
return;
|
|
285
|
+
if (plan.outcome.kind === "applied" && !plan.outcome.deletion && !verified.has(index))
|
|
286
|
+
return;
|
|
287
|
+
verifyReceipts.push(plan.receipt);
|
|
288
|
+
});
|
|
289
|
+
if (verifyReceipts.length > 0) {
|
|
290
|
+
const receiptBatch = buildAppendBatchRequests(context, [], verifyReceipts, { updatedAt });
|
|
291
|
+
const response = await this.runWrite(() => this.transport.batchUpdate({
|
|
292
|
+
spreadsheetId: this.spreadsheetId,
|
|
293
|
+
requests: receiptBatch.requests,
|
|
294
|
+
}));
|
|
295
|
+
requireValidBatchUpdateReply(response, receiptBatch.requests.length);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const results = [];
|
|
299
|
+
let includedCursor = 0;
|
|
300
|
+
bounded.forEach((effect, index) => {
|
|
301
|
+
if (schemaErrorIndices.has(index)) {
|
|
302
|
+
results.push(encodeSchemaErrorResult(effect, GOOGLE_SHEETS_API_EFFECT_REASONS.EFFECT_PAYLOAD_TOO_LARGE));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (index >= resolution.includeCount)
|
|
306
|
+
return;
|
|
307
|
+
const planIndex = includedCursor;
|
|
308
|
+
includedCursor += 1;
|
|
309
|
+
const plan = included[planIndex];
|
|
310
|
+
if (plan === undefined)
|
|
311
|
+
return;
|
|
312
|
+
let result = encodeOutcomeResult(plan.outcome);
|
|
313
|
+
if (postconditionMode === SYNC_POSTCONDITION_MODES.INLINE &&
|
|
314
|
+
plan.outcome.kind === "applied" &&
|
|
315
|
+
!plan.outcome.deletion &&
|
|
316
|
+
!verified.has(planIndex)) {
|
|
317
|
+
result = {
|
|
318
|
+
...result,
|
|
319
|
+
status: "retryable_error",
|
|
320
|
+
visibleRevision: absentValue(),
|
|
321
|
+
visibleHash: absentValue(),
|
|
322
|
+
reason: presentValue(GOOGLE_SHEETS_API_EFFECT_REASONS.POSTCONDITION_HASH_MISMATCH),
|
|
323
|
+
postcondition: "unavailable",
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
else if (postconditionMode === SYNC_POSTCONDITION_MODES.DEFERRED) {
|
|
327
|
+
result = withDeferredPostcondition(result);
|
|
328
|
+
}
|
|
329
|
+
results.push(result);
|
|
330
|
+
});
|
|
331
|
+
return {
|
|
332
|
+
results,
|
|
333
|
+
snapshotHash: absentValue(),
|
|
334
|
+
hasMore: bounded.length < request.effects.length || results.length < bounded.length,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/** Classifies one response-loss effect through a fresh target+receipt read. */
|
|
338
|
+
async readEffectPostcondition(effect) {
|
|
339
|
+
const [result] = await this.readEffectPostconditions({
|
|
340
|
+
physicalSheetId: effect.physicalSheetId,
|
|
341
|
+
sheetName: effect.payload.sheetName,
|
|
342
|
+
registeredRange: effect.payload.registeredRange,
|
|
343
|
+
projection: effect.projection,
|
|
344
|
+
schemaVersion: effect.payload.schemaVersion,
|
|
345
|
+
effects: [effect],
|
|
346
|
+
});
|
|
347
|
+
if (result === undefined) {
|
|
348
|
+
invalidProviderState("postcondition read returned no result");
|
|
349
|
+
}
|
|
350
|
+
return result.postcondition;
|
|
351
|
+
}
|
|
352
|
+
/** Classifies a recovery batch with one shared target+receipt read. */
|
|
353
|
+
async readEffectPostconditions(request) {
|
|
354
|
+
const definition = this.definitionForPhysicalSheet(request.physicalSheetId);
|
|
355
|
+
validateRoute(request, definition);
|
|
356
|
+
if (request.effects.length === 0) {
|
|
357
|
+
invalidProviderRequest("postcondition reads", "effects must not be empty");
|
|
358
|
+
}
|
|
359
|
+
const routeOptions = effectRouteOptions(definition);
|
|
360
|
+
const context = await this.readPreflight(request, definition, routeOptions);
|
|
361
|
+
return request.effects.map((effect) => ({
|
|
362
|
+
effectId: effect.effectId,
|
|
363
|
+
payloadHash: effect.payloadHash,
|
|
364
|
+
postcondition: classifyPostcondition(context, effect, context.receipts),
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
// -------------------------------------------------------------------------
|
|
368
|
+
// Projection provisioning (SyncSheetsProvisioner)
|
|
369
|
+
// -------------------------------------------------------------------------
|
|
370
|
+
/**
|
|
371
|
+
* Creates missing tabs and their header rows, or verifies existing tabs,
|
|
372
|
+
* in ONE atomic batchUpdate. An existing tab with no content anywhere in
|
|
373
|
+
* its used grid gets its headers initialized; an existing tab with content
|
|
374
|
+
* must match the registered headers exactly (order, duplicates, width),
|
|
375
|
+
* otherwise provisioning fails closed BEFORE any mutation. The operation
|
|
376
|
+
* is idempotent: a retry after a lost response re-enumerates, sees the
|
|
377
|
+
* exact headers, and succeeds without rewriting anything.
|
|
378
|
+
*/
|
|
379
|
+
async provisionRegistry(registrations) {
|
|
380
|
+
validateProvisionRegistrations(registrations);
|
|
381
|
+
// Enumerate every tab (no ranges, hidden included) with grid dimensions.
|
|
382
|
+
const enumerationRaw = await this.runRead(() => this.transport.getSpreadsheet({
|
|
383
|
+
spreadsheetId: this.spreadsheetId,
|
|
384
|
+
ranges: [],
|
|
385
|
+
fields: GOOGLE_SHEETS_API_PROVISION_ENUMERATION_FIELDS,
|
|
386
|
+
timeoutMs: this.readTimeoutMs,
|
|
387
|
+
}));
|
|
388
|
+
const sheets = parseSpreadsheetDocument(enumerationRaw, "provisioning enumeration");
|
|
389
|
+
const existingByTitle = new Map(sheets.sheets.map((sheet) => [sheet.title, sheet]));
|
|
390
|
+
// Read the full used grid of every existing registered tab (values plus
|
|
391
|
+
// formats). Missing tabs need no data read; they are created below.
|
|
392
|
+
const dataTargets = registrations.filter((registration) => existingByTitle.has(registration.sheetName));
|
|
393
|
+
const grids = new Map();
|
|
394
|
+
if (dataTargets.length > 0) {
|
|
395
|
+
const ranges = dataTargets.map((registration) => {
|
|
396
|
+
const existing = existingByTitle.get(registration.sheetName);
|
|
397
|
+
if (existing === undefined) {
|
|
398
|
+
invalidProviderState(`Registered sync sheet does not exist: ${registration.sheetName}`);
|
|
399
|
+
}
|
|
400
|
+
const endColumn = provisionGridEndColumn(registration, existing);
|
|
401
|
+
return `${quoteA1SheetName(registration.sheetName)}!A1:${columnLetters(endColumn)}1048576`;
|
|
402
|
+
});
|
|
403
|
+
const dataRaw = await this.runRead(() => this.transport.getSpreadsheet({
|
|
404
|
+
spreadsheetId: this.spreadsheetId,
|
|
405
|
+
ranges,
|
|
406
|
+
fields: GOOGLE_SHEETS_API_PROVISION_FIELDS,
|
|
407
|
+
timeoutMs: this.readTimeoutMs,
|
|
408
|
+
}));
|
|
409
|
+
const document = parseSpreadsheetDocument(dataRaw, "provisioning grid");
|
|
410
|
+
for (const registration of dataTargets) {
|
|
411
|
+
const existing = existingByTitle.get(registration.sheetName);
|
|
412
|
+
if (existing === undefined) {
|
|
413
|
+
invalidProviderState(`Registered sync sheet does not exist: ${registration.sheetName}`);
|
|
414
|
+
}
|
|
415
|
+
const grid = document.grids.get(existing.sheetId);
|
|
416
|
+
if (grid === undefined) {
|
|
417
|
+
invalidProviderState(`grid data is missing for sheet ${existing.sheetId}`);
|
|
418
|
+
}
|
|
419
|
+
grids.set(registration.sheetName, grid);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Plan all mutations: addSheet + header writes for missing tabs, header
|
|
423
|
+
// writes for truly-empty tabs, exact-match verification for content tabs.
|
|
424
|
+
const requests = [];
|
|
425
|
+
const createdSheets = [];
|
|
426
|
+
const initializedHeaders = [];
|
|
427
|
+
const usedSheetIds = new Set(sheets.sheets.map((sheet) => sheet.sheetId));
|
|
428
|
+
for (const registration of registrations) {
|
|
429
|
+
const range = parseRegisteredRange(registration.registeredRange);
|
|
430
|
+
const existing = existingByTitle.get(registration.sheetName);
|
|
431
|
+
const headerCells = registration.headers.map((header) => ({
|
|
432
|
+
userEnteredValue: { stringValue: header },
|
|
433
|
+
}));
|
|
434
|
+
if (existing === undefined) {
|
|
435
|
+
const sheetId = allocateSheetId(usedSheetIds);
|
|
436
|
+
usedSheetIds.add(sheetId);
|
|
437
|
+
requests.push({ kind: "addSheet", title: registration.sheetName, sheetId });
|
|
438
|
+
requests.push({
|
|
439
|
+
kind: "updateCells",
|
|
440
|
+
sheetId,
|
|
441
|
+
startRowIndex: 0,
|
|
442
|
+
startColumnIndex: range.startColumn - 1,
|
|
443
|
+
rows: [headerCells],
|
|
444
|
+
fields: "userEnteredValue",
|
|
445
|
+
});
|
|
446
|
+
createdSheets.push(registration.sheetName);
|
|
447
|
+
initializedHeaders.push(registration.sheetName);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const grid = grids.get(registration.sheetName);
|
|
451
|
+
if (grid === undefined) {
|
|
452
|
+
invalidProviderState(`provisioning grid is missing for ${registration.sheetName}`);
|
|
453
|
+
}
|
|
454
|
+
if (!gridHasContent(grid)) {
|
|
455
|
+
// Truly empty tab: initialize the header row only.
|
|
456
|
+
requests.push({
|
|
457
|
+
kind: "updateCells",
|
|
458
|
+
sheetId: existing.sheetId,
|
|
459
|
+
startRowIndex: 0,
|
|
460
|
+
startColumnIndex: range.startColumn - 1,
|
|
461
|
+
rows: [headerCells],
|
|
462
|
+
fields: "userEnteredValue",
|
|
463
|
+
});
|
|
464
|
+
initializedHeaders.push(registration.sheetName);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
// Content tab: the header row must match the registered schema exactly.
|
|
468
|
+
assertProvisioningHeaders(grid, registration);
|
|
469
|
+
}
|
|
470
|
+
if (requests.length > 0) {
|
|
471
|
+
const response = await this.runWrite(() => this.transport.batchUpdate({
|
|
472
|
+
spreadsheetId: this.spreadsheetId,
|
|
473
|
+
requests,
|
|
474
|
+
}));
|
|
475
|
+
requireValidBatchUpdateReply(response, requests.length);
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
registrations: registrations.map(({ headers: _headers, ...route }) => route),
|
|
479
|
+
createdSheets,
|
|
480
|
+
initializedHeaders,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
// -------------------------------------------------------------------------
|
|
484
|
+
// Values-only table reads (readRows / readRowsBatch)
|
|
485
|
+
// -------------------------------------------------------------------------
|
|
486
|
+
/** Reads one registered table's literal values with one REST read. */
|
|
487
|
+
async readRows(request) {
|
|
488
|
+
const [result] = await this.readRowsBatch([request]);
|
|
489
|
+
if (result === undefined) {
|
|
490
|
+
invalidProviderState("table read returned no result");
|
|
491
|
+
}
|
|
492
|
+
return result;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Reads several registered tables through ONE `spreadsheets.get` call.
|
|
496
|
+
* Results are returned in request order; a missing tab, header drift, or
|
|
497
|
+
* malformed payload fails closed before any result is produced.
|
|
498
|
+
*/
|
|
499
|
+
async readRowsBatch(requests) {
|
|
500
|
+
if (requests.length === 0)
|
|
501
|
+
return [];
|
|
502
|
+
const routes = requests.map((request) => {
|
|
503
|
+
const definition = this.definitionForPhysicalSheet(request.physicalSheetId);
|
|
504
|
+
validateRoute(request, definition);
|
|
505
|
+
if (request.headers.length !== definition.headers.length ||
|
|
506
|
+
request.headers.some((header, index) => header !== definition.headers[index])) {
|
|
507
|
+
throw new SyncSheetsContractError(SYNC_SHEETS_ERROR_CODES.INVALID_EFFECT_PAYLOAD, "sync provider table read headers do not match the registered projection " +
|
|
508
|
+
definition.sheet.physicalSheetId);
|
|
509
|
+
}
|
|
510
|
+
return { request, definition };
|
|
511
|
+
});
|
|
512
|
+
const raw = await this.runRead(() => this.transport.getSpreadsheet({
|
|
513
|
+
spreadsheetId: this.spreadsheetId,
|
|
514
|
+
ranges: routes.map(({ request }) => `${quoteA1SheetName(request.sheetName)}!A1:${rangeEndColumnLetters(request.registeredRange)}1048576`),
|
|
515
|
+
fields: GOOGLE_SHEETS_API_VALUES_FIELDS,
|
|
516
|
+
timeoutMs: this.readTimeoutMs,
|
|
517
|
+
}));
|
|
518
|
+
const document = parseSpreadsheetDocument(raw, "table read");
|
|
519
|
+
const results = [];
|
|
520
|
+
for (const { request, definition } of routes) {
|
|
521
|
+
const sheet = document.sheets.find((candidate) => candidate.title === request.sheetName);
|
|
522
|
+
if (sheet === undefined) {
|
|
523
|
+
invalidProviderState(`Registered sync sheet does not exist: ${request.sheetName}`);
|
|
524
|
+
}
|
|
525
|
+
const grid = document.grids.get(sheet.sheetId);
|
|
526
|
+
if (grid === undefined) {
|
|
527
|
+
invalidProviderState(`grid data is missing for sheet ${sheet.sheetId}`);
|
|
528
|
+
}
|
|
529
|
+
const rows = buildTableRowsFromGrid(grid, {
|
|
530
|
+
registeredRange: request.registeredRange,
|
|
531
|
+
headers: definition.headers,
|
|
532
|
+
checkboxHeaders: definition.checkboxHeaders ?? [],
|
|
533
|
+
});
|
|
534
|
+
results.push({
|
|
535
|
+
sheetName: request.sheetName,
|
|
536
|
+
registeredRange: request.registeredRange,
|
|
537
|
+
headers: [...definition.headers],
|
|
538
|
+
rows,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
return results;
|
|
542
|
+
}
|
|
543
|
+
// -------------------------------------------------------------------------
|
|
544
|
+
// Observation (anchors + snapshots)
|
|
545
|
+
// -------------------------------------------------------------------------
|
|
546
|
+
/**
|
|
547
|
+
* Ensures every nonblank row of one registered tab carries a
|
|
548
|
+
* developer-metadata anchor, writing all planned anchors in ONE atomic
|
|
549
|
+
* batchUpdate. Rows with more than one anchor fail closed; duplicate
|
|
550
|
+
* anchors across rows are reported as evidence. No re-read is performed.
|
|
551
|
+
*/
|
|
552
|
+
async ensureRowAnchors(request) {
|
|
553
|
+
const definition = this.definitionForPhysicalSheet(request.physicalSheetId);
|
|
554
|
+
validateRoute(request, definition);
|
|
555
|
+
const tabs = await this.readObservedTabs([request]);
|
|
556
|
+
const tab = tabs.get(request.sheetName);
|
|
557
|
+
if (tab === undefined) {
|
|
558
|
+
invalidProviderState(`Registered sync sheet does not exist: ${request.sheetName}`);
|
|
559
|
+
}
|
|
560
|
+
const plan = planRowAnchors(tab, {
|
|
561
|
+
registeredRange: request.registeredRange,
|
|
562
|
+
headers: definition.headers,
|
|
563
|
+
checkboxHeaders: definition.checkboxHeaders ?? [],
|
|
564
|
+
});
|
|
565
|
+
if (plan.planned.length > 0) {
|
|
566
|
+
await this.writeAnchors(tab, plan);
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
assigned: plan.assigned,
|
|
570
|
+
existing: plan.existing,
|
|
571
|
+
duplicateAnchors: plan.duplicateAnchors,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
/** Reads one full snapshot without any mutation (lock-free). */
|
|
575
|
+
async readSnapshot(request) {
|
|
576
|
+
const target = this.observationTargetFor(request);
|
|
577
|
+
const tabs = await this.readObservedTabs([request]);
|
|
578
|
+
const tab = tabs.get(request.sheetName);
|
|
579
|
+
if (tab === undefined) {
|
|
580
|
+
invalidProviderState(`Registered sync sheet does not exist: ${request.sheetName}`);
|
|
581
|
+
}
|
|
582
|
+
return buildSnapshotFromTab(tab, target);
|
|
583
|
+
}
|
|
584
|
+
/** Combines anchor assignment and one snapshot read under one request. */
|
|
585
|
+
async observeSnapshot(request) {
|
|
586
|
+
const [observed] = await this.observeSnapshots([request]);
|
|
587
|
+
if (observed === undefined) {
|
|
588
|
+
invalidProviderState("observation returned no result");
|
|
589
|
+
}
|
|
590
|
+
return observed;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Observes several projections with ONE grid read, ONE anchor write (when
|
|
594
|
+
* any anchor is missing), and ONE re-read (when anchors were written), so
|
|
595
|
+
* the committed anchors are reflected in every snapshot. The coordinator
|
|
596
|
+
* already holds every involved mutation lane before this call.
|
|
597
|
+
*/
|
|
598
|
+
async observeSnapshots(requests) {
|
|
599
|
+
if (requests.length === 0)
|
|
600
|
+
return [];
|
|
601
|
+
const targets = requests.map((request) => this.observationTargetFor(request));
|
|
602
|
+
const tabs = await this.readObservedTabs(requests);
|
|
603
|
+
const plans = [];
|
|
604
|
+
let assignedTotal = 0;
|
|
605
|
+
for (let index = 0; index < requests.length; index += 1) {
|
|
606
|
+
const request = requests[index];
|
|
607
|
+
const target = targets[index];
|
|
608
|
+
if (request === undefined || target === undefined) {
|
|
609
|
+
invalidProviderState("observation request is unavailable");
|
|
610
|
+
}
|
|
611
|
+
const tab = tabs.get(request.sheetName);
|
|
612
|
+
if (tab === undefined) {
|
|
613
|
+
invalidProviderState(`Registered sync sheet does not exist: ${request.sheetName}`);
|
|
614
|
+
}
|
|
615
|
+
const anchors = planRowAnchors(tab, {
|
|
616
|
+
registeredRange: request.registeredRange,
|
|
617
|
+
headers: target.headers,
|
|
618
|
+
checkboxHeaders: target.checkboxHeaders,
|
|
619
|
+
});
|
|
620
|
+
assignedTotal += anchors.assigned;
|
|
621
|
+
plans.push({ request, target, tab, anchors });
|
|
622
|
+
}
|
|
623
|
+
if (assignedTotal > 0) {
|
|
624
|
+
// One atomic write for every planned anchor across all requested tabs.
|
|
625
|
+
const anchorRequests = [];
|
|
626
|
+
for (const entry of plans) {
|
|
627
|
+
for (const planned of entry.anchors.planned) {
|
|
628
|
+
anchorRequests.push({
|
|
629
|
+
kind: "createDeveloperMetadata",
|
|
630
|
+
sheetId: entry.tab.sheetId,
|
|
631
|
+
rowIndex: planned.rowIndex,
|
|
632
|
+
key: GOOGLE_SHEETS_API_ANCHOR_KEY,
|
|
633
|
+
value: planned.anchor,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const response = await this.runWrite(() => this.transport.batchUpdate({
|
|
638
|
+
spreadsheetId: this.spreadsheetId,
|
|
639
|
+
requests: anchorRequests,
|
|
640
|
+
}));
|
|
641
|
+
requireValidBatchUpdateReply(response, anchorRequests.length);
|
|
642
|
+
// One shared re-read so the committed anchors appear in the snapshots
|
|
643
|
+
// (mirrors the Apps Script flush-while-locked behavior).
|
|
644
|
+
const refreshed = await this.readObservedTabs(requests);
|
|
645
|
+
for (const entry of plans) {
|
|
646
|
+
const tab = refreshed.get(entry.request.sheetName);
|
|
647
|
+
if (tab === undefined) {
|
|
648
|
+
invalidProviderState(`Registered sync sheet does not exist: ${entry.request.sheetName}`);
|
|
649
|
+
}
|
|
650
|
+
entry.tab = tab;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return plans.map((entry) => ({
|
|
654
|
+
anchors: {
|
|
655
|
+
assigned: entry.anchors.assigned,
|
|
656
|
+
existing: entry.anchors.existing,
|
|
657
|
+
duplicateAnchors: entry.anchors.duplicateAnchors,
|
|
658
|
+
},
|
|
659
|
+
snapshot: buildSnapshotFromTab(entry.tab, entry.target),
|
|
660
|
+
}));
|
|
661
|
+
}
|
|
662
|
+
// -------------------------------------------------------------------------
|
|
663
|
+
// Internal helpers
|
|
664
|
+
// -------------------------------------------------------------------------
|
|
665
|
+
/** Reads the target grids of one or more observation requests (one call). */
|
|
666
|
+
async readObservedTabs(requests) {
|
|
667
|
+
// One getSpreadsheet call serves the whole batch with ONE mask: the
|
|
668
|
+
// lighter user_input mask only when every request is lightweight (the
|
|
669
|
+
// lightweight branch never consults merges or dataValidation; the full
|
|
670
|
+
// mask is still correct for a mixed batch).
|
|
671
|
+
const lightweight = requests.length > 0 && requests.every((request) => request.readMode === SYNC_SNAPSHOT_READ_MODES.USER_INPUT);
|
|
672
|
+
const fields = lightweight
|
|
673
|
+
? GOOGLE_SHEETS_API_LIGHTWEIGHT_OBSERVATION_FIELDS
|
|
674
|
+
: GOOGLE_SHEETS_API_OBSERVATION_FIELDS;
|
|
675
|
+
return this.runRead(() => readTabGrids(this.transport, this.spreadsheetId, requests.map((request) => ({
|
|
676
|
+
sheetName: request.sheetName,
|
|
677
|
+
registeredRange: request.registeredRange,
|
|
678
|
+
})), fields, this.readTimeoutMs));
|
|
679
|
+
}
|
|
680
|
+
/** Writes every planned anchor of one tab in one atomic batch. */
|
|
681
|
+
async writeAnchors(tab, plan) {
|
|
682
|
+
const requests = plan.planned.map((planned) => ({
|
|
683
|
+
kind: "createDeveloperMetadata",
|
|
684
|
+
sheetId: tab.sheetId,
|
|
685
|
+
rowIndex: planned.rowIndex,
|
|
686
|
+
key: GOOGLE_SHEETS_API_ANCHOR_KEY,
|
|
687
|
+
value: planned.anchor,
|
|
688
|
+
}));
|
|
689
|
+
const response = await this.runWrite(() => this.transport.batchUpdate({
|
|
690
|
+
spreadsheetId: this.spreadsheetId,
|
|
691
|
+
requests,
|
|
692
|
+
}));
|
|
693
|
+
requireValidBatchUpdateReply(response, requests.length);
|
|
694
|
+
}
|
|
695
|
+
/** Validates one observation request and derives its snapshot target. */
|
|
696
|
+
observationTargetFor(request) {
|
|
697
|
+
const definition = this.definitionForPhysicalSheet(request.physicalSheetId);
|
|
698
|
+
validateRoute(request, definition);
|
|
699
|
+
// Fail closed on unknown readMode strings (same shared guard as the Apps
|
|
700
|
+
// Script observation operation) instead of silently reading in full mode.
|
|
701
|
+
const readMode = request.readMode === undefined
|
|
702
|
+
? SYNC_SNAPSHOT_READ_MODES.FULL
|
|
703
|
+
: requireSyncSnapshotReadMode(request.readMode, "Google Sheets API observation readMode", SYNC_SHEETS_ERROR_CODES.INVALID_EFFECT_PAYLOAD);
|
|
704
|
+
if (readMode === SYNC_SNAPSHOT_READ_MODES.USER_INPUT &&
|
|
705
|
+
request.projection !== SYNC_PROJECTIONS.USER_INPUT) {
|
|
706
|
+
invalidProviderRequest("observation", "user_input readMode requires the user_input projection");
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
sheetName: request.sheetName,
|
|
710
|
+
registeredRange: request.registeredRange,
|
|
711
|
+
projection: request.projection,
|
|
712
|
+
schemaVersion: request.schemaVersion,
|
|
713
|
+
headers: definition.headers,
|
|
714
|
+
checkboxHeaders: definition.checkboxHeaders ?? [],
|
|
715
|
+
readMode,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
/** Reads the target and receipt tabs for one route through the read lane. */
|
|
719
|
+
async readPreflight(request, definition, routeOptions) {
|
|
720
|
+
// Each preflight performs two paced transport calls: a range-less sheet
|
|
721
|
+
// enumeration (hidden receipt tab discovery) plus one ranged data read.
|
|
722
|
+
const sheets = await this.runRead(() => enumerateSheetProperties(this.transport, this.spreadsheetId, this.readTimeoutMs));
|
|
723
|
+
return this.runRead(() => readPreflightData(this.transport, {
|
|
724
|
+
spreadsheetId: this.spreadsheetId,
|
|
725
|
+
sheetName: request.sheetName,
|
|
726
|
+
registeredRange: request.registeredRange,
|
|
727
|
+
headers: definition.headers,
|
|
728
|
+
identityField: routeOptions.identityField,
|
|
729
|
+
checkboxHeaders: routeOptions.checkboxHeaders,
|
|
730
|
+
}, sheets, this.readTimeoutMs));
|
|
731
|
+
}
|
|
732
|
+
/** Paces ONE `getSpreadsheet` transport call and emits one read event. */
|
|
733
|
+
async runRead(task) {
|
|
734
|
+
await this.readLimiter.waitForSlot();
|
|
735
|
+
const startedAt = this.now();
|
|
736
|
+
try {
|
|
737
|
+
const result = await task();
|
|
738
|
+
this.emitRequest("getSpreadsheet", 1, startedAt, true, absentValue(), absentValue());
|
|
739
|
+
return result;
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
const outcome = classifyTransportOutcome(error);
|
|
743
|
+
this.emitRequest("getSpreadsheet", 1, startedAt, false, outcome.httpStatus, outcome.code);
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
/** Paces ONE `batchUpdate` transport call and emits one write event. */
|
|
748
|
+
async runWrite(task) {
|
|
749
|
+
await this.writeLimiter.waitForSlot();
|
|
750
|
+
const startedAt = this.now();
|
|
751
|
+
try {
|
|
752
|
+
const result = await task();
|
|
753
|
+
this.emitRequest("batchUpdate", 1, startedAt, true, absentValue(), absentValue());
|
|
754
|
+
return result;
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
const outcome = classifyTransportOutcome(error);
|
|
758
|
+
this.emitRequest("batchUpdate", 1, startedAt, false, outcome.httpStatus, outcome.code);
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
emitRequest(operation, operationCount, startedAt, ok, httpStatus, code) {
|
|
763
|
+
try {
|
|
764
|
+
this.onRequest?.({
|
|
765
|
+
operation,
|
|
766
|
+
operationCount,
|
|
767
|
+
startedAt,
|
|
768
|
+
durationMs: Math.max(0, this.now() - startedAt),
|
|
769
|
+
ok,
|
|
770
|
+
httpStatus,
|
|
771
|
+
code,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
catch {
|
|
775
|
+
// Diagnostics must never change a remote result.
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
definitionForPhysicalSheet(physicalSheetId) {
|
|
779
|
+
const definition = this.definitions.find((candidate) => candidate.sheet.physicalSheetId === physicalSheetId);
|
|
780
|
+
if (definition === undefined) {
|
|
781
|
+
throw new SyncSheetsContractError(SYNC_SHEETS_ERROR_CODES.INVALID_PROVISIONING_DEFINITIONS, "no projection definition exists for " + physicalSheetId);
|
|
782
|
+
}
|
|
783
|
+
return definition;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
// ---------------------------------------------------------------------------
|
|
787
|
+
// Outbound helpers (ported from the Apps Script effect operations)
|
|
788
|
+
// ---------------------------------------------------------------------------
|
|
789
|
+
/** Builds the fixed-shape receipt record used by the fast-append path. */
|
|
790
|
+
function makeAppendReceipt(effectId, payloadHash, visibleHash) {
|
|
791
|
+
return {
|
|
792
|
+
effectId,
|
|
793
|
+
payloadHash,
|
|
794
|
+
status: "applied",
|
|
795
|
+
visibleHash,
|
|
796
|
+
visibleRevision: 1,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
/** Validates the append rows before any remote read or write. */
|
|
800
|
+
function validateAppendRows(rows) {
|
|
801
|
+
const seenEffectIds = new Set();
|
|
802
|
+
for (const row of rows) {
|
|
803
|
+
if (row.effectId.length === 0 || seenEffectIds.has(row.effectId)) {
|
|
804
|
+
invalidProviderRequest("fast append", "effectIds must be non-empty and unique");
|
|
805
|
+
}
|
|
806
|
+
seenEffectIds.add(row.effectId);
|
|
807
|
+
const payloadHash = row.payloadHash;
|
|
808
|
+
if (payloadHash === undefined || payloadHash.length === 0) {
|
|
809
|
+
invalidProviderRequest("fast append", "payloadHash is required");
|
|
810
|
+
}
|
|
811
|
+
if (row.anchor !== undefined && row.anchor.length === 0) {
|
|
812
|
+
invalidProviderRequest("fast append", "row anchor must be non-empty");
|
|
813
|
+
}
|
|
814
|
+
const fields = row.fields;
|
|
815
|
+
if (fields === null || typeof fields !== "object" || Array.isArray(fields)) {
|
|
816
|
+
invalidProviderRequest("fast append", "row fields must be an object");
|
|
817
|
+
}
|
|
818
|
+
if (Object.keys(fields).length === 0) {
|
|
819
|
+
invalidProviderRequest("fast append", "row fields are required");
|
|
820
|
+
}
|
|
821
|
+
for (const value of Object.values(fields)) {
|
|
822
|
+
if (!isNormalizedCell(value)) {
|
|
823
|
+
invalidProviderRequest("fast append", "row fields contain an invalid normalized cell");
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
/** Append rows must cover exactly the registered headers (batch append rule). */
|
|
829
|
+
function validateAppendRowsAgainstHeaders(rows, headers) {
|
|
830
|
+
const expected = [...headers].sort();
|
|
831
|
+
for (const row of rows) {
|
|
832
|
+
const actual = Object.keys(row.fields).sort();
|
|
833
|
+
if (actual.length !== expected.length ||
|
|
834
|
+
actual.some((field, index) => field !== expected[index])) {
|
|
835
|
+
invalidProviderRequest("fast append", "rows must contain exactly the registered headers");
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Derives the append identity from the row's identity field cell, using the
|
|
841
|
+
* canonical identity rule (non-empty string or finite number) shared with
|
|
842
|
+
* the append/replay paths.
|
|
843
|
+
*/
|
|
844
|
+
function appendIdentity(row, identityField) {
|
|
845
|
+
const cell = row.fields[identityField] ?? null;
|
|
846
|
+
const identity = identityFromNormalizedCell(cell);
|
|
847
|
+
if (identity === null) {
|
|
848
|
+
invalidProviderRequest("fast append", `sync identity is required for append: ${identityField}`);
|
|
849
|
+
}
|
|
850
|
+
return identity;
|
|
851
|
+
}
|
|
852
|
+
/** Finds a preflight row by its visible identity (single match or fail closed). */
|
|
853
|
+
function findRowByIdentity(context, identity) {
|
|
854
|
+
const matches = context.rows.filter((row) => row.identity.kind === "present" && row.identity.value === identity);
|
|
855
|
+
if (matches.length > 1) {
|
|
856
|
+
invalidProviderState(`sync identity is duplicated: ${identity}`);
|
|
857
|
+
}
|
|
858
|
+
return matches[0];
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Mirrors the built-in append identity preflight: every existing data row
|
|
862
|
+
* needs a unique identity, and every pending non-replay row needs a fresh,
|
|
863
|
+
* non-empty identity.
|
|
864
|
+
*/
|
|
865
|
+
function assertAppendIdentityAvailability(context, identityField, pending) {
|
|
866
|
+
const existing = new Map();
|
|
867
|
+
context.rows.forEach((row) => {
|
|
868
|
+
if (row.identity.kind !== "present") {
|
|
869
|
+
invalidProviderState(`sync identity is missing at row ${row.rowNumber}`);
|
|
870
|
+
}
|
|
871
|
+
const location = existing.get(row.identity.value);
|
|
872
|
+
if (location !== undefined) {
|
|
873
|
+
invalidProviderState(`sync identity is duplicated: ${row.identity.value} at rows ${location} and ${row.rowNumber}`);
|
|
874
|
+
}
|
|
875
|
+
existing.set(row.identity.value, String(row.rowNumber));
|
|
876
|
+
});
|
|
877
|
+
for (const row of pending) {
|
|
878
|
+
const identity = appendIdentity(row, identityField);
|
|
879
|
+
const location = existing.get(identity);
|
|
880
|
+
if (location !== undefined) {
|
|
881
|
+
invalidProviderState(`sync identity already exists: ${identity} at ${location}`);
|
|
882
|
+
}
|
|
883
|
+
existing.set(identity, "pending");
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/** Builds a working row for one pending append at its reserved position. */
|
|
887
|
+
function toAppendWorkingRow(row, rowNumber) {
|
|
888
|
+
// The fast path never materializes anchor metadata (the Apps Script batch
|
|
889
|
+
// append path ignores the advisory row anchor); the row replays by identity.
|
|
890
|
+
return {
|
|
891
|
+
rowNumber,
|
|
892
|
+
anchor: absentValue(),
|
|
893
|
+
cells: { ...row.fields },
|
|
894
|
+
identity: absentValue(),
|
|
895
|
+
appended: true,
|
|
896
|
+
deleted: false,
|
|
897
|
+
writeFields: {},
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
/** Locates one planned write's row in a fresh verification context. */
|
|
901
|
+
function findProbeRowInContext(context, plan) {
|
|
902
|
+
const mutation = plan.mutation;
|
|
903
|
+
if (mutation === undefined)
|
|
904
|
+
return undefined;
|
|
905
|
+
if (mutation.kind === "append") {
|
|
906
|
+
return context.rows.find((row) => row.rowNumber === mutation.row.rowNumber);
|
|
907
|
+
}
|
|
908
|
+
const anchor = mutation.row.anchor;
|
|
909
|
+
if (anchor.kind === "present") {
|
|
910
|
+
return context.rows.find((row) => row.physicalAnchor.kind === "present" && row.physicalAnchor.value === anchor.value);
|
|
911
|
+
}
|
|
912
|
+
const identity = mutation.row.identity;
|
|
913
|
+
if (identity.kind === "present") {
|
|
914
|
+
return context.rows.find((row) => row.identity.kind === "present" && row.identity.value === identity.value);
|
|
915
|
+
}
|
|
916
|
+
return undefined;
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Route validation against the registered definition (mirrors
|
|
920
|
+
* `validateRoute` in the Apps Script operation provider).
|
|
921
|
+
*/
|
|
922
|
+
function validateRoute(request, definition) {
|
|
923
|
+
if (request.sheetName !== definition.sheet.tabName ||
|
|
924
|
+
request.registeredRange !== definition.sheet.registeredRange ||
|
|
925
|
+
request.projection !== definition.sheet.projection ||
|
|
926
|
+
request.schemaVersion !== definition.sheet.schemaVersion) {
|
|
927
|
+
throw new SyncSheetsContractError(SYNC_SHEETS_ERROR_CODES.INVALID_EFFECT_PAYLOAD, "sync provider request does not match the registered projection " +
|
|
928
|
+
definition.sheet.physicalSheetId);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
/** Derives the per-route effect options exactly like the Apps Script provider. */
|
|
932
|
+
function effectRouteOptions(definition) {
|
|
933
|
+
const identityField = definition.sheet.projection === "system_state"
|
|
934
|
+
? definition.sheet.businessKeyField
|
|
935
|
+
: definition.sheet.projection === "sync_conflicts"
|
|
936
|
+
? "Conflict_ID"
|
|
937
|
+
: undefined;
|
|
938
|
+
return {
|
|
939
|
+
identityField: identityField === undefined
|
|
940
|
+
? absentValue()
|
|
941
|
+
: presentValue(identityField),
|
|
942
|
+
checkboxHeaders: definition.checkboxHeaders ?? [],
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Validates a batchUpdate reply shape: one reply per request, with the
|
|
947
|
+
* addSheet reply carrying the created sheet id. A malformed 2xx response must
|
|
948
|
+
* not close effects, so this throws a delivery-uncertain state error.
|
|
949
|
+
*/
|
|
950
|
+
function requireValidBatchUpdateReply(value, requestCount) {
|
|
951
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
952
|
+
invalidProviderState("batchUpdate response must be an object");
|
|
953
|
+
}
|
|
954
|
+
const record = value;
|
|
955
|
+
if (!Array.isArray(record.replies) || record.replies.length !== requestCount) {
|
|
956
|
+
invalidProviderState(`batchUpdate reply count does not match ${requestCount} requests`);
|
|
957
|
+
}
|
|
958
|
+
record.replies.forEach((reply, index) => {
|
|
959
|
+
if (reply === null || typeof reply !== "object" || Array.isArray(reply)) {
|
|
960
|
+
invalidProviderState(`batchUpdate reply[${index}] must be an object`);
|
|
961
|
+
}
|
|
962
|
+
const replyRecord = reply;
|
|
963
|
+
const addSheet = replyRecord.addSheet;
|
|
964
|
+
if (addSheet === undefined)
|
|
965
|
+
return;
|
|
966
|
+
if (addSheet === null || typeof addSheet !== "object" || Array.isArray(addSheet)) {
|
|
967
|
+
invalidProviderState(`batchUpdate reply[${index}].addSheet is invalid`);
|
|
968
|
+
}
|
|
969
|
+
const properties = addSheet.properties;
|
|
970
|
+
if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
|
|
971
|
+
invalidProviderState(`batchUpdate reply[${index}].addSheet.properties is invalid`);
|
|
972
|
+
}
|
|
973
|
+
if (typeof properties.sheetId !== "number") {
|
|
974
|
+
invalidProviderState(`batchUpdate reply[${index}].addSheet.properties.sheetId is invalid`);
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
// ---------------------------------------------------------------------------
|
|
979
|
+
// Provisioning helpers
|
|
980
|
+
// ---------------------------------------------------------------------------
|
|
981
|
+
/** Validates provisioning registrations before any transport call. */
|
|
982
|
+
function validateProvisionRegistrations(registrations) {
|
|
983
|
+
if (registrations.length === 0) {
|
|
984
|
+
invalidProviderRequest("provisioning", "registrations must not be empty");
|
|
985
|
+
}
|
|
986
|
+
const tabNames = new Set();
|
|
987
|
+
for (const registration of registrations) {
|
|
988
|
+
if (registration.sheetName.trim() === "") {
|
|
989
|
+
invalidProviderRequest("provisioning", "sheetName must be non-empty");
|
|
990
|
+
}
|
|
991
|
+
if (tabNames.has(registration.sheetName)) {
|
|
992
|
+
invalidProviderRequest("provisioning", `cannot repeat a tab name: ${registration.sheetName}`);
|
|
993
|
+
}
|
|
994
|
+
tabNames.add(registration.sheetName);
|
|
995
|
+
const range = parseRegisteredRange(registration.registeredRange);
|
|
996
|
+
if (range.columnCount !== registration.headers.length) {
|
|
997
|
+
invalidProviderRequest("provisioning", `headers do not match registeredRange for ${registration.sheetName}`);
|
|
998
|
+
}
|
|
999
|
+
if (registration.headers.length === 0 ||
|
|
1000
|
+
registration.headers.some((header) => header.trim() === "")) {
|
|
1001
|
+
invalidProviderRequest("provisioning", `headers must contain non-empty names for ${registration.sheetName}`);
|
|
1002
|
+
}
|
|
1003
|
+
if (new Set(registration.headers).size !== registration.headers.length) {
|
|
1004
|
+
invalidProviderRequest("provisioning", `headers must not contain duplicates for ${registration.sheetName}`);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Builds the provisioning data-read end column: the sheet's actual grid
|
|
1010
|
+
* width when the enumeration supplied it (so content anywhere in the tab
|
|
1011
|
+
* decides emptiness), otherwise the registered range's end column and
|
|
1012
|
+
* emptiness is judged only from the returned grid.
|
|
1013
|
+
*/
|
|
1014
|
+
function provisionGridEndColumn(registration, existing) {
|
|
1015
|
+
const parsed = parseRegisteredRange(registration.registeredRange);
|
|
1016
|
+
const registeredEnd = parsed.startColumn + parsed.columnCount - 1;
|
|
1017
|
+
const gridColumns = existing.gridProperties?.columnCount;
|
|
1018
|
+
if (gridColumns === undefined)
|
|
1019
|
+
return registeredEnd;
|
|
1020
|
+
return Math.max(gridColumns, registeredEnd);
|
|
1021
|
+
}
|
|
1022
|
+
/** Builds the A1 range end letters for one registered range. */
|
|
1023
|
+
function rangeEndColumnLetters(registeredRange) {
|
|
1024
|
+
const range = parseRegisteredRange(registeredRange);
|
|
1025
|
+
return columnLetters(range.startColumn + range.columnCount - 1);
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Returns whether a provisioning grid has any content anywhere.
|
|
1029
|
+
*
|
|
1030
|
+
* Only cells with an actual ENTERED value count (a userEnteredValue carrying
|
|
1031
|
+
* stringValue/numberValue/boolValue/formulaValue). Blank `{}` cells and
|
|
1032
|
+
* format-only cells (userEnteredFormat without a value) are ignored, matching
|
|
1033
|
+
* the Apps Script `getLastRow()/getLastColumn()` semantics provisioning was
|
|
1034
|
+
* ported from — a blank-but-formatted tab is still initialized, never judged
|
|
1035
|
+
* as a content tab that must match headers.
|
|
1036
|
+
*/
|
|
1037
|
+
function gridHasContent(grid) {
|
|
1038
|
+
for (const row of grid.rowData) {
|
|
1039
|
+
for (const value of row.values) {
|
|
1040
|
+
if (cellHasEnteredValue(value))
|
|
1041
|
+
return true;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
/** Returns whether one API cell carries a real entered value. */
|
|
1047
|
+
function cellHasEnteredValue(value) {
|
|
1048
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
1049
|
+
return false;
|
|
1050
|
+
const entered = value.userEnteredValue;
|
|
1051
|
+
if (entered === null || typeof entered !== "object" || Array.isArray(entered))
|
|
1052
|
+
return false;
|
|
1053
|
+
const enteredRecord = entered;
|
|
1054
|
+
return enteredRecord.stringValue !== undefined ||
|
|
1055
|
+
enteredRecord.numberValue !== undefined ||
|
|
1056
|
+
enteredRecord.boolValue !== undefined ||
|
|
1057
|
+
enteredRecord.formulaValue !== undefined;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Verifies one content tab's header row against the registered schema:
|
|
1061
|
+
* exact order, non-empty strings, no duplicates, and full registered-range
|
|
1062
|
+
* coverage. Any drift fails closed BEFORE any mutation, mirroring the Apps
|
|
1063
|
+
* Script "operational provisioning header mismatch" behavior (including a
|
|
1064
|
+
* blank header row on a tab whose content lives outside the registered
|
|
1065
|
+
* range).
|
|
1066
|
+
*/
|
|
1067
|
+
function assertProvisioningHeaders(grid, registration) {
|
|
1068
|
+
const range = parseRegisteredRange(registration.registeredRange);
|
|
1069
|
+
const headerValues = gridHeaderCells(grid, range);
|
|
1070
|
+
const actual = headerValues.map((value, index) => {
|
|
1071
|
+
const raw = provisioningHeaderString(value);
|
|
1072
|
+
if (raw === null) {
|
|
1073
|
+
invalidProviderState(`operational provisioning header mismatch: ${registration.sheetName}` +
|
|
1074
|
+
` (header is missing at column ${index + 1})`);
|
|
1075
|
+
}
|
|
1076
|
+
return raw;
|
|
1077
|
+
});
|
|
1078
|
+
if (new Set(actual).size !== actual.length) {
|
|
1079
|
+
invalidProviderState(`operational provisioning header mismatch: ${registration.sheetName} (duplicate header)`);
|
|
1080
|
+
}
|
|
1081
|
+
if (actual.length !== registration.headers.length ||
|
|
1082
|
+
actual.some((header, index) => header !== registration.headers[index])) {
|
|
1083
|
+
invalidProviderState(`operational provisioning header mismatch: ${registration.sheetName}`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Reads a provisioning header cell as its raw string: string values as-is,
|
|
1088
|
+
* numbers and booleans stringified (the Apps Script source compares
|
|
1089
|
+
* `String(actual) === String(expected)`), anything else treated as missing.
|
|
1090
|
+
*/
|
|
1091
|
+
function provisioningHeaderString(value) {
|
|
1092
|
+
if (value === null || typeof value !== "object")
|
|
1093
|
+
return null;
|
|
1094
|
+
const record = value;
|
|
1095
|
+
const entered = record.userEnteredValue;
|
|
1096
|
+
if (entered === null || typeof entered !== "object")
|
|
1097
|
+
return null;
|
|
1098
|
+
const enteredRecord = entered;
|
|
1099
|
+
if (enteredRecord.stringValue !== undefined) {
|
|
1100
|
+
return typeof enteredRecord.stringValue === "string" ? enteredRecord.stringValue : null;
|
|
1101
|
+
}
|
|
1102
|
+
if (enteredRecord.numberValue !== undefined) {
|
|
1103
|
+
return typeof enteredRecord.numberValue === "number" && Number.isFinite(enteredRecord.numberValue)
|
|
1104
|
+
? String(enteredRecord.numberValue)
|
|
1105
|
+
: null;
|
|
1106
|
+
}
|
|
1107
|
+
if (enteredRecord.boolValue !== undefined) {
|
|
1108
|
+
return typeof enteredRecord.boolValue === "boolean" ? String(enteredRecord.boolValue) : null;
|
|
1109
|
+
}
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
//# sourceMappingURL=GoogleSheetsApiSyncProvider.js.map
|