@fabrika/provider-zerops 0.9.0-dev.3 → 0.9.0-dev.4

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.
Files changed (2) hide show
  1. package/package.json +9 -9
  2. package/src/provider.ts +56 -40
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/provider-zerops",
3
- "version": "0.9.0-dev.3",
3
+ "version": "0.9.0-dev.4",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -44,14 +44,14 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "zod": "^3.25.76",
47
- "@fabrika/auth": "0.9.0-dev.3",
48
- "@fabrika/auth-core": "0.9.0-dev.3",
49
- "@fabrika/email": "0.9.0-dev.3",
50
- "@fabrika/iam-contract": "0.9.0-dev.3",
51
- "@fabrika/installation-init": "0.9.0-dev.3",
52
- "@fabrika/operations-contract": "0.9.0-dev.3",
53
- "@fabrika/provider-contract": "0.9.0-dev.3",
54
- "@fabrika/proxy-core": "0.9.0-dev.3",
47
+ "@fabrika/auth": "0.9.0-dev.4",
48
+ "@fabrika/auth-core": "0.9.0-dev.4",
49
+ "@fabrika/email": "0.9.0-dev.4",
50
+ "@fabrika/iam-contract": "0.9.0-dev.4",
51
+ "@fabrika/installation-init": "0.9.0-dev.4",
52
+ "@fabrika/operations-contract": "0.9.0-dev.4",
53
+ "@fabrika/provider-contract": "0.9.0-dev.4",
54
+ "@fabrika/proxy-core": "0.9.0-dev.4",
55
55
  "yaml": "^2.9.1"
56
56
  }
57
57
  }
package/src/provider.ts CHANGED
@@ -620,64 +620,80 @@ const writeServiceEnv = async (env: StepEnv): Promise<void> => {
620
620
  assertRunning(signal)
621
621
  // One read: every write below waits for its own user-data sync, so the build request never meets `userDataSyncRunning`.
622
622
  const records = await zerops.api.listServiceEnv({ serviceId, signal })
623
+ const changes: ServiceEnvChange[] = []
624
+ const put = (key: string, value: string, sensitive: boolean): void => {
625
+ changes.push({ line: `${key}: updated`, apply: () => zerops.api.putServiceEnv({ serviceId, key, value, sensitive, signal }) })
626
+ }
627
+ const remove = (key: string, envId: string): void => {
628
+ changes.push({ line: `${key}: removed`, apply: () => zerops.api.deleteServiceEnv({ envId, signal }) })
629
+ }
623
630
  for (const key of SERVICE_MANAGED_KEYS) {
624
631
  const value = managedValues[key]
625
632
  const existing = records.find((record) => record.key === key)
626
633
  const sensitive = SENSITIVE_MANAGED_KEYS.has(key)
627
- assertRunning(signal)
628
- if (value !== null && (sensitive || readableContent(existing) !== value)) {
629
- await zerops.api.putServiceEnv({ serviceId, key, value, sensitive, signal })
630
- log(` ${key}: updated`)
631
- } else if (value === null && existing !== undefined) {
632
- await zerops.api.deleteServiceEnv({ envId: existing.id, signal })
633
- log(` ${key}: removed`)
634
- } else {
635
- log(` ${key}: unchanged`)
636
- }
634
+ if (value !== null && (sensitive || readableContent(existing) !== value)) put(key, value, sensitive)
635
+ else if (value === null && existing !== undefined) remove(key, existing.id)
636
+ else changes.push({ line: `${key}: unchanged`, apply: null })
637
637
  }
638
638
  const provenance = env.attempt.storageProvenance
639
639
  const marker = records.find((record) => record.key === STORAGE_PROVENANCE_KEY)
640
- if (typeof provenance === 'string' && readableContent(marker) !== provenance) {
641
- assertRunning(signal)
642
- // Not a credential, and the exposure read-back above reads it: it must stay readable.
643
- await zerops.api.putServiceEnv({ serviceId, key: STORAGE_PROVENANCE_KEY, value: provenance, sensitive: false, signal })
644
- log(` ${STORAGE_PROVENANCE_KEY}: updated`)
645
- } else if (provenance === null && marker !== undefined) {
646
- assertRunning(signal)
647
- await zerops.api.deleteServiceEnv({ envId: marker.id, signal })
648
- log(` ${STORAGE_PROVENANCE_KEY}: removed`)
649
- }
640
+ // Not a credential, and the exposure read-back above reads it: it must stay readable.
641
+ if (typeof provenance === 'string' && readableContent(marker) !== provenance) put(STORAGE_PROVENANCE_KEY, provenance, false)
642
+ else if (provenance === null && marker !== undefined) remove(STORAGE_PROVENANCE_KEY, marker.id)
650
643
  // A workload key is written like a managed key; one whose workload is gone is removed, so no stale key stays readable.
651
644
  for (const [key, value] of Object.entries(familyVariables)) {
652
645
  const existing = records.find((record) => record.key === key)
653
646
  const sensitive = isSensitiveFamilyVariable(key)
654
- assertRunning(signal)
655
- if (sensitive || readableContent(existing) !== value) {
656
- await zerops.api.putServiceEnv({ serviceId, key, value, sensitive, signal })
657
- log(` ${key}: updated`)
658
- } else {
659
- log(` ${key}: unchanged`)
660
- }
647
+ if (sensitive || readableContent(existing) !== value) put(key, value, sensitive)
648
+ else changes.push({ line: `${key}: unchanged`, apply: null })
661
649
  }
662
650
  for (const record of records) {
663
- if (isFrameworkFamilyVariable(record.key) && !Object.hasOwn(familyVariables, record.key)) {
664
- assertRunning(signal)
665
- await zerops.api.deleteServiceEnv({ envId: record.id, signal })
666
- log(` ${record.key}: removed`)
667
- }
651
+ if (isFrameworkFamilyVariable(record.key) && !Object.hasOwn(familyVariables, record.key)) remove(record.key, record.id)
668
652
  }
669
653
  // The build lifts the upload's bearer from here (`${RUNTIME_…}`), so the descriptor never holds it.
670
654
  const bearer = env.run.artifactUpload?.bearer ?? null
671
655
  const uploadKey = records.find((record) => record.key === FABRIKA_SOURCE_MAP_UPLOAD_KEY)
672
- if (bearer !== null) {
673
- assertRunning(signal)
674
- await zerops.api.putServiceEnv({ serviceId, key: FABRIKA_SOURCE_MAP_UPLOAD_KEY, value: bearer, sensitive: true, signal })
675
- log(` ${FABRIKA_SOURCE_MAP_UPLOAD_KEY}: updated`)
676
- } else if (uploadKey !== undefined) {
677
- assertRunning(signal)
678
- await zerops.api.deleteServiceEnv({ envId: uploadKey.id, signal })
679
- log(` ${FABRIKA_SOURCE_MAP_UPLOAD_KEY}: removed`)
656
+ if (bearer !== null) put(FABRIKA_SOURCE_MAP_UPLOAD_KEY, bearer, true)
657
+ else if (uploadKey !== undefined) remove(FABRIKA_SOURCE_MAP_UPLOAD_KEY, uploadKey.id)
658
+ await applyServiceEnvChanges(changes, log, signal)
659
+ }
660
+
661
+ /** One service variable write or removal `write-service-env` decided on, or a key it leaves as it is (`apply: null`). */
662
+ interface ServiceEnvChange {
663
+ readonly line: string
664
+ readonly apply: (() => Promise<void>) | null
665
+ }
666
+
667
+ /** Each write waits ~3 s for its own user-data sync; concurrent writes to one service do not conflict (zerops-platform.md). */
668
+ const SERVICE_ENV_WRITE_CONCURRENCY = 6
669
+
670
+ /**
671
+ * Applies the changes `SERVICE_ENV_WRITE_CONCURRENCY` at a time and waits for every started one, so no sync still runs
672
+ * when the step ends. A failure or a cancel starts no further write; lines follow the decision order.
673
+ */
674
+ const applyServiceEnvChanges = async (changes: readonly ServiceEnvChange[], log: (line: string) => void, signal: AbortSignal): Promise<void> => {
675
+ const applied = new Set<number>()
676
+ const failures: unknown[] = []
677
+ let next = 0
678
+ const worker = async (): Promise<void> => {
679
+ while (next < changes.length && failures.length === 0 && !signal.aborted) {
680
+ const index = next++
681
+ const apply = changes[index]?.apply ?? null
682
+ if (apply === null) continue
683
+ try {
684
+ await apply()
685
+ applied.add(index)
686
+ } catch (error) {
687
+ failures.push(error)
688
+ }
689
+ }
690
+ }
691
+ await Promise.all(Array.from({ length: SERVICE_ENV_WRITE_CONCURRENCY }, worker))
692
+ for (const [index, change] of changes.entries()) {
693
+ if (change.apply === null || applied.has(index)) log(` ${change.line}`)
680
694
  }
695
+ if (failures.length > 0) throw failures[0]
696
+ assertRunning(signal)
681
697
  }
682
698
 
683
699
  /** The custom autoscaling with its minimum RAM raised to `minRam`; every other field is sent back as it was read. */