velocious 1.0.574 → 1.0.576

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 (82) hide show
  1. package/README.md +17 -3
  2. package/build/background-jobs/client.js +71 -3
  3. package/build/background-jobs/job.js +30 -0
  4. package/build/background-jobs/main.js +100 -14
  5. package/build/background-jobs/store.js +344 -41
  6. package/build/background-jobs/types.js +25 -1
  7. package/build/background-jobs/web/controller.js +1 -0
  8. package/build/database/advisory-lock-runner.js +27 -17
  9. package/build/database/drivers/mssql/index.js +98 -3
  10. package/build/database/record/index.js +10 -8
  11. package/build/database/table-data/table-column.js +2 -0
  12. package/build/frontend-models/resource-definition.js +1 -138
  13. package/build/src/background-jobs/client.d.ts +30 -0
  14. package/build/src/background-jobs/client.d.ts.map +1 -1
  15. package/build/src/background-jobs/client.js +64 -4
  16. package/build/src/background-jobs/job.d.ts +19 -0
  17. package/build/src/background-jobs/job.d.ts.map +1 -1
  18. package/build/src/background-jobs/job.js +27 -1
  19. package/build/src/background-jobs/main.d.ts +41 -0
  20. package/build/src/background-jobs/main.d.ts.map +1 -1
  21. package/build/src/background-jobs/main.js +95 -14
  22. package/build/src/background-jobs/store.d.ts +136 -0
  23. package/build/src/background-jobs/store.d.ts.map +1 -1
  24. package/build/src/background-jobs/store.js +314 -42
  25. package/build/src/background-jobs/types.d.ts +86 -2
  26. package/build/src/background-jobs/types.d.ts.map +1 -1
  27. package/build/src/background-jobs/types.js +26 -2
  28. package/build/src/background-jobs/web/controller.d.ts.map +1 -1
  29. package/build/src/background-jobs/web/controller.js +2 -1
  30. package/build/src/database/advisory-lock-runner.d.ts +27 -14
  31. package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
  32. package/build/src/database/advisory-lock-runner.js +27 -18
  33. package/build/src/database/drivers/mssql/index.d.ts +32 -0
  34. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  35. package/build/src/database/drivers/mssql/index.js +86 -4
  36. package/build/src/database/record/index.d.ts +12 -8
  37. package/build/src/database/record/index.d.ts.map +1 -1
  38. package/build/src/database/record/index.js +11 -9
  39. package/build/src/database/table-data/table-column.d.ts.map +1 -1
  40. package/build/src/database/table-data/table-column.js +4 -1
  41. package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
  42. package/build/src/frontend-models/resource-definition.js +2 -124
  43. package/build/src/sync/local-mutation-log.d.ts +7 -1
  44. package/build/src/sync/local-mutation-log.d.ts.map +1 -1
  45. package/build/src/sync/local-mutation-log.js +18 -7
  46. package/build/src/sync/peer-mutation-bundle.d.ts.map +1 -1
  47. package/build/src/sync/peer-mutation-bundle.js +9 -7
  48. package/build/src/sync/signed-sync-envelope-replay-service.d.ts.map +1 -1
  49. package/build/src/sync/signed-sync-envelope-replay-service.js +3 -1
  50. package/build/src/sync/sync-envelope-replay-service.d.ts +69 -0
  51. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  52. package/build/src/sync/sync-envelope-replay-service.js +186 -18
  53. package/build/src/testing/test-runner.d.ts.map +1 -1
  54. package/build/src/testing/test-runner.js +17 -22
  55. package/build/src/utils/sha256-hex.d.ts +10 -0
  56. package/build/src/utils/sha256-hex.d.ts.map +1 -0
  57. package/build/src/utils/sha256-hex.js +126 -0
  58. package/build/sync/local-mutation-log.js +19 -5
  59. package/build/sync/peer-mutation-bundle.js +8 -6
  60. package/build/sync/signed-sync-envelope-replay-service.js +2 -0
  61. package/build/sync/sync-envelope-replay-service.js +205 -17
  62. package/build/testing/test-runner.js +17 -22
  63. package/build/tsconfig.tsbuildinfo +1 -1
  64. package/build/utils/sha256-hex.js +141 -0
  65. package/package.json +1 -1
  66. package/src/background-jobs/client.js +71 -3
  67. package/src/background-jobs/job.js +30 -0
  68. package/src/background-jobs/main.js +100 -14
  69. package/src/background-jobs/store.js +344 -41
  70. package/src/background-jobs/types.js +25 -1
  71. package/src/background-jobs/web/controller.js +1 -0
  72. package/src/database/advisory-lock-runner.js +27 -17
  73. package/src/database/drivers/mssql/index.js +98 -3
  74. package/src/database/record/index.js +10 -8
  75. package/src/database/table-data/table-column.js +2 -0
  76. package/src/frontend-models/resource-definition.js +1 -138
  77. package/src/sync/local-mutation-log.js +19 -5
  78. package/src/sync/peer-mutation-bundle.js +8 -6
  79. package/src/sync/signed-sync-envelope-replay-service.js +2 -0
  80. package/src/sync/sync-envelope-replay-service.js +205 -17
  81. package/src/testing/test-runner.js +17 -22
  82. package/src/utils/sha256-hex.js +141 -0
@@ -3,7 +3,10 @@
3
3
  import {deliverDeclaredBroadcasts, upsertSyncRow} from "./sync-change-fanout.js"
4
4
  import {markServerApply} from "./sync-publish-suppression.js"
5
5
  import {resolveFrontendModelResourceClass} from "../frontend-models/resource-definition.js"
6
+ import {resolveSyncConflict} from "./conflict-strategy.js"
6
7
  import SyncReplayUpsertApplier from "./sync-replay-upsert-applier.js"
8
+ import stableJsonStringify from "./stable-json.js"
9
+ import sha256Hex from "../utils/sha256-hex.js"
7
10
  import {ValidationError} from "../database/record/index.js"
8
11
  import VelociousError from "../velocious-error.js"
9
12
 
@@ -16,6 +19,8 @@ import VelociousError from "../velocious-error.js"
16
19
  */
17
20
  /**
18
21
  * @typedef {object} SyncReplayMutation
22
+ * @property {string | number | null} [baseVersion] - Base server/client version observed by the client.
23
+ * @property {string} [clientMutationId] - Original client mutation id from the signed envelope.
19
24
  * @property {Date} clientUpdatedAt - Client-side mutation timestamp.
20
25
  * @property {Record<string, ?>} data - Parsed mutation payload.
21
26
  * @property {?} id - Client sync row id for per-sync responses.
@@ -65,6 +70,7 @@ export default class SyncEnvelopeReplayService {
65
70
  * @param {(broadcast: {channel: string, params: Record<string, ?>, body: ?}) => Promise<void>} [args.broadcaster] - Delivers declarative broadcasts. Required when broadcasts are configured.
66
71
  * @param {SyncReplayBroadcast[]} [args.broadcasts] - Broadcasts fanned out by the default afterReplayMutation.
67
72
  * @param {import("../configuration.js").default} [args.configuration] - Configuration whose frontend-model registry routes mutations to resource classes.
73
+ * @param {{strategy?: "optimisticVersion" | "serverWins", versionAttribute: string} | null} [args.conflictStrategy] - Optional base-version conflict detection for routed upserts. Only `optimisticVersion` and `serverWins` are supported for backend replay because the server does not have the client's base snapshot. When `strategy` is omitted it defaults to `optimisticVersion`, matching `resolveSyncConflict` and normalized resource config. When configured, a mutation whose baseVersion does not match the current server versionAttribute is rejected with a structured conflict result instead of being applied.
68
74
  * @param {Record<string, import("../configuration-types.js").FrontendModelResourceClassType | string>} [args.resourceTypeOverrides] - Per-resourceType routing overrides: a resource class, or a string alias resolved through the registry.
69
75
  * @param {import("../authorization/ability.js").default} [args.ability] - Ability scoping routed record lookups and create membership checks.
70
76
  * @param {Record<string, ?>} [args.abilityContext] - Ability context passed to routed resources.
@@ -83,6 +89,7 @@ export default class SyncEnvelopeReplayService {
83
89
  this.broadcasts = args.broadcasts || null
84
90
  this.applyHandlers = args.applyHandlers ? this.builtApplyHandlers(args.applyHandlers) : null
85
91
  this.configuration = args.configuration || null
92
+ this.conflictStrategy = args.conflictStrategy || null
86
93
  this.resourceTypeOverrides = args.resourceTypeOverrides || null
87
94
  this.ability = args.ability || null
88
95
  this.abilityContext = args.abilityContext || null
@@ -96,6 +103,16 @@ export default class SyncEnvelopeReplayService {
96
103
  if (this.broadcasts && !this.broadcaster) {
97
104
  throw new Error("SyncEnvelopeReplayService broadcasts require a broadcaster option delivering them")
98
105
  }
106
+ if (this.conflictStrategy) {
107
+ const supportedConflictStrategies = new Set(["optimisticVersion", "serverWins"])
108
+
109
+ if (!this.conflictStrategy.versionAttribute || typeof this.conflictStrategy.versionAttribute !== "string") {
110
+ throw new Error("SyncEnvelopeReplayService conflictStrategy requires a non-blank versionAttribute")
111
+ }
112
+ if (this.conflictStrategy.strategy !== undefined && !supportedConflictStrategies.has(this.conflictStrategy.strategy)) {
113
+ throw new Error(`Unsupported sync conflict strategy for backend replay: ${this.conflictStrategy.strategy}. Only optimisticVersion and serverWins are supported.`)
114
+ }
115
+ }
99
116
  }
100
117
 
101
118
  /**
@@ -181,6 +198,15 @@ export default class SyncEnvelopeReplayService {
181
198
  throw error
182
199
  }
183
200
 
201
+ if (applyResult && applyResult.status === "conflict") {
202
+ syncResponses.push({
203
+ conflict: applyResult.conflict,
204
+ id: mutation.id,
205
+ syncState: "conflict"
206
+ })
207
+ continue
208
+ }
209
+
184
210
  await this.persistReplayMutation({actor: actorResult.actor, context, existingSync, applyResult, mutation, shouldApply})
185
211
  await this.afterReplayMutation({actor: actorResult.actor, context, existingSync, applyResult, mutation, shouldApply})
186
212
 
@@ -249,7 +275,7 @@ export default class SyncEnvelopeReplayService {
249
275
  }
250
276
 
251
277
  const sync = /** @type {Record<string, ?>} */ (rawSync)
252
- const {clientUpdatedAt, data, id, resourceId, resourceType, syncType} = sync
278
+ const {clientMutationId, clientUpdatedAt, data, id, resourceId, resourceType, syncType} = sync
253
279
 
254
280
  if (typeof resourceType !== "string" || resourceType.length < 1 || resourceId === undefined || resourceId === null || typeof syncType !== "string" || syncType.length < 1) {
255
281
  return {ok: false, response: {id, syncState: "failed", reason: "invalid-resource-id"}}
@@ -267,6 +293,8 @@ export default class SyncEnvelopeReplayService {
267
293
  return {
268
294
  ok: true,
269
295
  mutation: {
296
+ baseVersion: sync.baseVersion,
297
+ clientMutationId,
270
298
  clientUpdatedAt: clientUpdatedAtDate,
271
299
  data: normalizedDataResult.data,
272
300
  id,
@@ -644,29 +672,160 @@ export default class SyncEnvelopeReplayService {
644
672
  */
645
673
  async applyRoutedReplayUpsert({context, mutation, resource}) {
646
674
  const attributes = this.permittedRoutedAttributes({mutation, resource})
647
- const existingRecord = await resource.findSyncRecord({mutation})
675
+ const ModelClass = resource.modelClass()
676
+ const runUpsert = async () => {
677
+ const existingRecord = await resource.findSyncRecord({mutation})
678
+ const conflictResult = await this.routedReplayConflictResult({attributes, existingRecord, mutation, resource})
648
679
 
649
- /** @type {import("../database/record/index.js").default | null} */
650
- let record = existingRecord
651
- let created = false
680
+ if (conflictResult) return conflictResult
652
681
 
653
- if (existingRecord) {
654
- const releaseServerApply = markServerApply(existingRecord)
682
+ /** @type {import("../database/record/index.js").default | null} */
683
+ let record = existingRecord
684
+ let created = false
655
685
 
656
- try {
657
- existingRecord.assign(attributes)
658
- await this.saveRoutedReplayRecord(existingRecord)
659
- } finally {
660
- releaseServerApply()
686
+ if (existingRecord) {
687
+ const releaseServerApply = markServerApply(existingRecord)
688
+
689
+ try {
690
+ existingRecord.assign(attributes)
691
+ await this.saveRoutedReplayRecord(existingRecord)
692
+ } finally {
693
+ releaseServerApply()
694
+ }
695
+ } else {
696
+ record = await this.createRoutedReplayRecord({attributes, mutation, resource})
697
+ created = true
698
+ }
699
+
700
+ const extras = await resource.afterSyncApply({context, created, mutation, record})
701
+
702
+ return {created, deleted: false, record, ...extras}
703
+ }
704
+
705
+ if (!this.conflictStrategy) return await runUpsert()
706
+
707
+ return await ModelClass.withAdvisoryLock(syncReplayConflictLockName({resourceId: mutation.resourceId, resourceType: mutation.resourceType}), runUpsert, {dedicatedConnection: true})
708
+ }
709
+
710
+ /**
711
+ * Checks whether a routed upsert mutation conflicts with the current server
712
+ * state when the service is configured with a conflict strategy. A mutation
713
+ * whose baseVersion does not match the server's current versionAttribute is
714
+ * rejected with a structured conflict payload instead of being applied.
715
+ * @param {object} args - Conflict-check args.
716
+ * @param {Record<string, ?>} args.attributes - Permitted mutation attributes.
717
+ * @param {import("../database/record/index.js").default | null} args.existingRecord - Existing server record.
718
+ * @param {import("./sync-envelope-replay-service.js").SyncReplayMutation} args.mutation - Normalized replay mutation.
719
+ * @param {import("../frontend-model-resource/base-resource.js").default} args.resource - Routed resource instance.
720
+ * @returns {Promise<Record<string, ?> | null>} - Conflict apply result, or null when no conflict.
721
+ */
722
+ async routedReplayConflictResult({attributes, existingRecord, mutation, resource}) {
723
+ if (!this.conflictStrategy) return null
724
+ if (!existingRecord || mutation.syncType === "create") return null
725
+ if (mutation.baseVersion === undefined || mutation.baseVersion === null) return null
726
+
727
+ const versionAttribute = this.conflictStrategy.versionAttribute
728
+ const serverVersion = normalizeConflictValue(existingRecord.readAttribute(versionAttribute))
729
+
730
+ if (stableJsonStringify(serverVersion) === stableJsonStringify(mutation.baseVersion)) return null
731
+
732
+ const ModelClass = resource.modelClass()
733
+ const primaryKey = ModelClass.primaryKey()
734
+ const serializedAffectedAttributes = await this.serializedRoutedConflictAttributes({attributes, existingRecord, resource})
735
+ const serverAttributes = {
736
+ ...serializedAffectedAttributes,
737
+ [primaryKey]: existingRecord.readAttribute(primaryKey),
738
+ [versionAttribute]: serverVersion
739
+ }
740
+
741
+ const serverRecord = {
742
+ attributes: serverAttributes,
743
+ version: serverVersion
744
+ }
745
+ const conflictMutation = /** @type {import("./device-identity.js").SyncMutation} */ (/** @type {unknown} */ ({
746
+ attributes,
747
+ baseVersion: mutation.baseVersion,
748
+ clientMutationId: mutation.clientMutationId || mutation.id,
749
+ model: mutation.resourceType,
750
+ operation: mutation.syncType,
751
+ payload: {id: mutation.resourceId}
752
+ }))
753
+ const result = await resolveSyncConflict({
754
+ baseRecord: null,
755
+ mutation: conflictMutation,
756
+ serverRecord,
757
+ strategy: this.conflictStrategy.strategy || "optimisticVersion",
758
+ versionAttribute
759
+ })
760
+
761
+ if (result.status !== "conflict") return null
762
+
763
+ return {conflict: result.conflict, created: false, deleted: false, record: existingRecord, status: "conflict"}
764
+ }
765
+
766
+ /**
767
+ * Projects affected mutation fields through the resource's readable
768
+ * attribute contract. Writable-but-hidden fields are omitted, while custom
769
+ * `<attribute>Attribute(model)` serializers and model accessors remain the
770
+ * source of frontend-visible values. The full model attribute hash is never
771
+ * exposed.
772
+ * @param {object} args - Projection args.
773
+ * @param {Record<string, ?>} args.attributes - Permitted affected mutation attributes.
774
+ * @param {import("../database/record/index.js").default} args.existingRecord - Authorized server record.
775
+ * @param {import("../frontend-model-resource/base-resource.js").default} args.resource - Routed resource instance.
776
+ * @returns {Promise<Record<string, ?>>} Serialized readable affected attributes.
777
+ */
778
+ async serializedRoutedConflictAttributes({attributes, existingRecord, resource}) {
779
+ const ModelClass = resource.modelClass()
780
+ const ResourceClass = /** @type {import("../configuration-types.js").FrontendModelResourceClassType} */ (resource.constructor)
781
+ const readableAttributes = new Set()
782
+ const configuredAttributes = ResourceClass.resourceConfig().attributes
783
+ const configuredEntries = Array.isArray(configuredAttributes) ? configuredAttributes : Object.keys(configuredAttributes)
784
+
785
+ if (configuredEntries.length === 0) {
786
+ const attributeNameToColumnName = ModelClass.getAttributeNameToColumnNameMap()
787
+
788
+ for (const attributeName of Object.keys(attributeNameToColumnName)) {
789
+ readableAttributes.add(attributeName)
661
790
  }
662
- } else {
663
- record = await this.createRoutedReplayRecord({attributes, mutation, resource})
664
- created = true
665
791
  }
666
792
 
667
- const extras = await resource.afterSyncApply({context, created, mutation, record})
793
+ for (const configuredAttribute of configuredEntries) {
794
+ const configuredName = typeof configuredAttribute === "string" ? configuredAttribute : configuredAttribute.name
795
+
796
+ if (!configuredName) continue
797
+
798
+ const canonicalName = ModelClass.resolveAttributeName(configuredName)
799
+
800
+ readableAttributes.add(canonicalName || configuredName)
801
+ }
802
+
803
+ /** @type {Record<string, ?>} */
804
+ const serializedAttributes = {}
805
+
806
+ for (const affectedField of Object.keys(attributes)) {
807
+ const attributeName = ModelClass.resolveAttributeName(affectedField)
808
+
809
+ if (!attributeName || !readableAttributes.has(attributeName)) continue
668
810
 
669
- return {created, deleted: false, record, ...extras}
811
+ const resourceAttribute = resource.resourceMethod(`${attributeName}Attribute`)
812
+
813
+ if (resourceAttribute) {
814
+ serializedAttributes[affectedField] = normalizeConflictValue(await resourceAttribute.method.call(resourceAttribute.resource, existingRecord))
815
+ continue
816
+ }
817
+
818
+ const recordMethods = /** @type {Record<string, ?>} */ (/** @type {unknown} */ (existingRecord))
819
+ const attributeMethod = recordMethods[attributeName]
820
+
821
+ if (typeof attributeMethod === "function") {
822
+ serializedAttributes[affectedField] = normalizeConflictValue(await attributeMethod.call(existingRecord))
823
+ } else {
824
+ serializedAttributes[affectedField] = normalizeConflictValue(existingRecord.readAttribute(attributeName))
825
+ }
826
+ }
827
+
828
+ return serializedAttributes
670
829
  }
671
830
 
672
831
  /**
@@ -890,3 +1049,32 @@ export default class SyncEnvelopeReplayService {
890
1049
  await deliverDeclaredBroadcasts({args, broadcaster: this.broadcaster, broadcasts: this.broadcasts})
891
1050
  }
892
1051
  }
1052
+
1053
+ /**
1054
+ * Returns a deterministic, MySQL-safe advisory-lock name for a routed replay
1055
+ * resource identity. The full `{resourceType, resourceId}` identity is hashed
1056
+ * with SHA-256 and truncated to 32 hex characters so the final name stays well
1057
+ * under MySQL/MariaDB's 64-character `GET_LOCK` limit while remaining
1058
+ * collision-resistant.
1059
+ * @param {object} args - Lock identity args.
1060
+ * @param {string} args.resourceId - Resource id.
1061
+ * @param {string} args.resourceType - Resource type.
1062
+ * @returns {string} - Advisory lock name.
1063
+ */
1064
+ export function syncReplayConflictLockName({resourceId, resourceType}) {
1065
+ const identity = stableJsonStringify({resourceId, resourceType})
1066
+ const hash = sha256Hex(identity).slice(0, 32)
1067
+
1068
+ return `vsr:${hash}`
1069
+ }
1070
+
1071
+ /**
1072
+ * Normalizes an authoritative conflict value for JSON transport and deterministic comparison.
1073
+ * @param {?} value - Raw value from a database record.
1074
+ * @returns {?} - Normalized value (Date values become ISO strings).
1075
+ */
1076
+ function normalizeConflictValue(value) {
1077
+ if (value instanceof Date) return value.toISOString()
1078
+
1079
+ return value
1080
+ }
@@ -828,24 +828,22 @@ export default class TestRunner {
828
828
  process.on("uncaughtException", onUncaughtException)
829
829
 
830
830
  try {
831
- await this.getConfiguration().ensureConnections({name: "Test runner suite"}, async () => {
832
- await this.runTests({
833
- afterEaches: [],
834
- beforeEaches: [],
835
- tests,
836
- descriptions: [],
837
- indentLevel: 0
838
- })
839
-
840
- // A rejection scheduled by the final test (a detached rejected promise,
841
- // or an afterCommit callback rejecting as the suite drains) is reported
842
- // by Node on a LATER turn. Drain a few turns while the handler is still
843
- // attached — and connections still open — so those late rejections are
844
- // recorded instead of escaping to the default crash path after cleanup.
845
- for (let drainTurn = 0; drainTurn < 3; drainTurn++) {
846
- await new Promise((resolve) => setImmediate(resolve))
847
- }
831
+ await this.runTests({
832
+ afterEaches: [],
833
+ beforeEaches: [],
834
+ tests,
835
+ descriptions: [],
836
+ indentLevel: 0
848
837
  })
838
+
839
+ // A rejection scheduled by the final test (a detached rejected promise,
840
+ // or an afterCommit callback rejecting as the suite drains) is reported
841
+ // by Node on a LATER turn. Drain a few turns while the handler is still
842
+ // attached so those late rejections are recorded instead of escaping to
843
+ // the default crash path after cleanup.
844
+ for (let drainTurn = 0; drainTurn < 3; drainTurn++) {
845
+ await new Promise((resolve) => setImmediate(resolve))
846
+ }
849
847
  } finally {
850
848
  process.off("unhandledRejection", onUnhandledRejection)
851
849
  process.off("uncaughtException", onUncaughtException)
@@ -1005,11 +1003,8 @@ export default class TestRunner {
1005
1003
  // Pin one connection per test so beforeEach, the test body and afterEach
1006
1004
  // all run on the SAME connection. This is required for transaction-based
1007
1005
  // database cleaning (beforeEach starts a transaction, afterEach rolls it
1008
- // back). ensureConnections reuses the suite-level pinned connection while
1009
- // it is healthy and transparently re-establishes a per-test pin if an
1010
- // earlier spec closed the suite connection (which would otherwise leave a
1011
- // stale async-context pin and force every later test onto a fresh checkout,
1012
- // breaking isolation).
1006
+ // back). Releasing the lease after each lifecycle also runs the pool's
1007
+ // session cleanup before another test can reuse the connection.
1013
1008
  await this.getConfiguration().ensureConnections({name: `Test: ${testDescription}`}, async () => {
1014
1009
  // Register dynamic candidates before application hooks so transaction
1015
1010
  // state changes made during a hook are visible to a request dispatched
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Computes SHA-256 without importing Node-only crypto modules, keeping this
3
+ * module safe for Expo/browser bundles. Implementation is a straightforward
4
+ * pure-JavaScript SHA-256 derived from FIPS 180-4, using only 32-bit unsigned
5
+ * integer arithmetic.
6
+ * @param {string} message - UTF-8 message.
7
+ * @returns {string} - Hex digest.
8
+ */
9
+ export default function sha256Hex(message) {
10
+ const bytes = utf8Bytes(message)
11
+ const padded = [...bytes]
12
+ const bitLength = bytes.length * 8
13
+ const hash = [...SHA256_INITIAL_HASH]
14
+ /** @type {number[]} */
15
+ const words = new Array(64)
16
+
17
+ padded.push(0x80)
18
+ while (padded.length % 64 !== 56) padded.push(0)
19
+
20
+ const highLength = Math.floor(bitLength / 0x100000000)
21
+ const lowLength = bitLength >>> 0
22
+
23
+ for (const value of [highLength, lowLength]) {
24
+ padded.push((value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff)
25
+ }
26
+
27
+ for (let offset = 0; offset < padded.length; offset += 64) {
28
+ for (let i = 0; i < 16; i++) {
29
+ const index = offset + (i * 4)
30
+
31
+ words[i] = (((padded[index] || 0) << 24) | ((padded[index + 1] || 0) << 16) | ((padded[index + 2] || 0) << 8) | (padded[index + 3] || 0)) >>> 0
32
+ }
33
+
34
+ for (let i = 16; i < 64; i++) {
35
+ const s0 = rotateRight(words[i - 15], 7) ^ rotateRight(words[i - 15], 18) ^ (words[i - 15] >>> 3)
36
+ const s1 = rotateRight(words[i - 2], 17) ^ rotateRight(words[i - 2], 19) ^ (words[i - 2] >>> 10)
37
+
38
+ words[i] = add32(words[i - 16], s0, words[i - 7], s1)
39
+ }
40
+
41
+ let [a, b, c, d, e, f, g, h] = hash
42
+
43
+ for (let i = 0; i < 64; i++) {
44
+ const s1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25)
45
+ const ch = (e & f) ^ ((~e) & g)
46
+ const temp1 = add32(h, s1, ch, SHA256_K[i], words[i])
47
+ const s0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22)
48
+ const maj = (a & b) ^ (a & c) ^ (b & c)
49
+ const temp2 = add32(s0, maj)
50
+
51
+ h = g
52
+ g = f
53
+ f = e
54
+ e = add32(d, temp1)
55
+ d = c
56
+ c = b
57
+ b = a
58
+ a = add32(temp1, temp2)
59
+ }
60
+
61
+ hash[0] = add32(hash[0], a)
62
+ hash[1] = add32(hash[1], b)
63
+ hash[2] = add32(hash[2], c)
64
+ hash[3] = add32(hash[3], d)
65
+ hash[4] = add32(hash[4], e)
66
+ hash[5] = add32(hash[5], f)
67
+ hash[6] = add32(hash[6], g)
68
+ hash[7] = add32(hash[7], h)
69
+ }
70
+
71
+ return hash.map((value) => value.toString(16).padStart(8, "0")).join("")
72
+ }
73
+
74
+ const SHA256_INITIAL_HASH = [
75
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
76
+ 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
77
+ ]
78
+
79
+ const SHA256_K = [
80
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
81
+ 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
82
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
83
+ 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
84
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
85
+ 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
86
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
87
+ 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
88
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
89
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
90
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
91
+ 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
92
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
93
+ 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
94
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
95
+ 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
96
+ ]
97
+
98
+ /**
99
+ * Converts a string to UTF-8 bytes.
100
+ * @param {string} value - String value.
101
+ * @returns {number[]} - UTF-8 bytes.
102
+ */
103
+ function utf8Bytes(value) {
104
+ /** @type {number[]} */
105
+ const bytes = []
106
+
107
+ for (const character of value) {
108
+ const codePoint = /** @type {number} */ (character.codePointAt(0))
109
+
110
+ if (codePoint <= 0x7f) {
111
+ bytes.push(codePoint)
112
+ } else if (codePoint <= 0x7ff) {
113
+ bytes.push(0xc0 | (codePoint >>> 6), 0x80 | (codePoint & 0x3f))
114
+ } else if (codePoint <= 0xffff) {
115
+ bytes.push(0xe0 | (codePoint >>> 12), 0x80 | ((codePoint >>> 6) & 0x3f), 0x80 | (codePoint & 0x3f))
116
+ } else {
117
+ bytes.push(0xf0 | (codePoint >>> 18), 0x80 | ((codePoint >>> 12) & 0x3f), 0x80 | ((codePoint >>> 6) & 0x3f), 0x80 | (codePoint & 0x3f))
118
+ }
119
+ }
120
+
121
+ return bytes
122
+ }
123
+
124
+ /**
125
+ * Adds unsigned 32-bit integers.
126
+ * @param {...number} values - Values to add.
127
+ * @returns {number} - Unsigned 32-bit result.
128
+ */
129
+ function add32(...values) {
130
+ return values.reduce((sum, value) => (sum + value) >>> 0, 0)
131
+ }
132
+
133
+ /**
134
+ * Rotates a 32-bit integer right.
135
+ * @param {number} value - Value to rotate.
136
+ * @param {number} bits - Bit count.
137
+ * @returns {number} - Rotated value.
138
+ */
139
+ function rotateRight(value, bits) {
140
+ return (value >>> bits) | (value << (32 - bits))
141
+ }