chuvsu-js 5.1.2 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -181,7 +181,11 @@ const client = new TimetableClient({
181
181
 
182
182
  `TimetableRepositoryAdapter` можно реализовать поверх БД, KV-хранилища или
183
183
  файлового сервиса. Запись использует ревизии compare-and-set, чтобы параллельные
184
- процессы не перезаписывали установленные ID.
184
+ процессы не перезаписывали установленные ID. Для БД реализуйте
185
+ `compareAndSetPatch`: обычное обновление передает только измененные сущности и
186
+ полные замены затронутых источников. `compareAndSet` с полным снимком оставлен
187
+ как совместимый fallback; его не следует использовать на горячем пути большого
188
+ репозитория.
185
189
 
186
190
  ```ts
187
191
  const snapshot = await client.exportRepository();
@@ -2,4 +2,8 @@
2
2
  export declare const GLOBALSIGN_GCC_R3_DV_TLS_CA_2020 = "-----BEGIN CERTIFICATE-----\nMIIEsDCCA5igAwIBAgIQd70OB0LV2enQSdd00CpvmjANBgkqhkiG9w0BAQsFADBM\nMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xv\nYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjAeFw0yMDA3MjgwMDAwMDBaFw0y\nOTAzMTgwMDAwMDBaMFMxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWdu\nIG52LXNhMSkwJwYDVQQDEyBHbG9iYWxTaWduIEdDQyBSMyBEViBUTFMgQ0EgMjAy\nMDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKxnlJV/de+OpwyvCXAJ\nIcxPCqkFPh1lttW2oljS3oUqPKq8qX6m7K0OVKaKG3GXi4CJ4fHVUgZYE6HRdjqj\nhhnuHY6EBCBegcUFgPG0scB12Wi8BHm9zKjWxo3Y2bwhO8Fvr8R42pW0eINc6OTb\nQXC0VWFCMVzpcqgz6X49KMZowAMFV6XqtItcG0cMS//9dOJs4oBlpuqX9INxMTGp\n6EASAF9cnlAGy/RXkVS9nOLCCa7pCYV+WgDKLTF+OK2Vxw3RUJ/p8009lQeUARv2\nUCcNNPCifYX1xIspvarkdjzLwzOdLahDdQbJON58zN4V+lMj0msg+c0KnywPIRp3\nBMkCAwEAAaOCAYUwggGBMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEF\nBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUDZjA\nc3+rvb3ZR0tJrQpKDKw+x3wwHwYDVR0jBBgwFoAUj/BLf6guRSSuTVD6Y5qL3uLd\nG7wwewYIKwYBBQUHAQEEbzBtMC4GCCsGAQUFBzABhiJodHRwOi8vb2NzcDIuZ2xv\nYmFsc2lnbi5jb20vcm9vdHIzMDsGCCsGAQUFBzAChi9odHRwOi8vc2VjdXJlLmds\nb2JhbHNpZ24uY29tL2NhY2VydC9yb290LXIzLmNydDA2BgNVHR8ELzAtMCugKaAn\nhiVodHRwOi8vY3JsLmdsb2JhbHNpZ24uY29tL3Jvb3QtcjMuY3JsMEcGA1UdIARA\nMD4wPAYEVR0gADA0MDIGCCsGAQUFBwIBFiZodHRwczovL3d3dy5nbG9iYWxzaWdu\nLmNvbS9yZXBvc2l0b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAy8j/c550ea86oCkf\nr2W+ptTCYe6iVzvo7H0V1vUEADJOWelTv07Obf+YkEatdN1Jg09ctgSNv2h+LMTk\nKRZdAXmsE3N5ve+z1Oa9kuiu7284LjeS09zHJQB4DJJJkvtIbjL/ylMK1fbMHhAW\ni0O194TWvH3XWZGXZ6ByxTUIv1+kAIql/Mt29PmKraTT5jrzcVzQ5A9jw16yysuR\nXRrLODlkS1hyBjsfyTNZrmL1h117IFgntBA5SQNVl9ckedq5r4RSAU85jV8XK5UL\nREjRZt2I6M9Po9QL7guFLu4sPFJpwR1sPJvubS2THeo7SxYoNDtdyBHs7euaGcMa\nD/fayQ==\n-----END CERTIFICATE-----";
3
3
  /** GlobalSign Root CA - R3 */
4
4
  export declare const GLOBALSIGN_ROOT_R3 = "-----BEGIN CERTIFICATE-----\nMIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G\nA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp\nZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4\nMTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG\nA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8\nRgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT\ngHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm\nKPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd\nQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ\nXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw\nDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o\nLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU\nRUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp\njjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK\n6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX\nmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs\nMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH\nWD9f\n-----END CERTIFICATE-----";
5
+ /** GlobalSign GCC R46 DV TLS CA 2025 (intermediate) */
6
+ export declare const GLOBALSIGN_GCC_R46_DV_TLS_CA_2025 = "-----BEGIN CERTIFICATE-----\nMIIFfDCCA2SgAwIBAgIRAIRDWIEh21qyhJn/roBnC6QwDQYJKoZIhvcNAQELBQAw\nRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNV\nBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwHhcNMjUwOTE3MDI1NTQ0WhcNMjkwNjIz\nMDAwMDAwWjBUMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1z\nYTEqMCgGA1UEAxMhR2xvYmFsU2lnbiBHQ0MgUjQ2IERWIFRMUyBDQSAyMDI1MIIB\nIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApT+6pung92y3k3sp2FQMmKv2\nOqcOBNr9uk8IJUf/EyIHKfYRcIdgRuUu5T2PxIsvGYCAHQ3uSG/p6E1a16aMu9LV\nEG9DMXcW+rx6Why5IDOIOvRHjfPyQzAGurD4V/e71U4fW81n7pKIDtcWDFeSXoai\nt1cemN088vJH/L4iCSeQQ1qWsmo9VV4F8jrwWJEjOSrLLYBGHvvuteMlXzpjX2A7\npKcjOooEYU78ygS2K0YOdyN7W2hIUvScVv5yG3sA/q4/tWXY1kyUC/Bb70IgL0/n\nwAYMia+OgDo5RPb+U8RUsBN4QH9qxgM75nBiOlqh/aTrrMYfYrbWOR3vioZQDQID\nAQABo4IBVTCCAVEwDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMB\nMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFAhp6X+lq/TDYeXNjntH74sp\nZI3sMB8GA1UdIwQYMBaAFANcq3OBh6jMsKbVlOI2lkn/BZksMHsGCCsGAQUFBwEB\nBG8wbTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuZ2xvYmFsc2lnbi5jb20vcm9v\ndHI0NjA7BggrBgEFBQcwAoYvaHR0cDovL3NlY3VyZS5nbG9iYWxzaWduLmNvbS9j\nYWNlcnQvcm9vdHI0Ni5jcnQwNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n\nbG9iYWxzaWduLmNvbS9yb290cjQ2LmNybDAhBgNVHSAEGjAYMAgGBmeBDAECATAM\nBgorBgEEAaAyCgEDMA0GCSqGSIb3DQEBCwUAA4ICAQAqoA/ThPjFFFZKSDkoKTKx\nKA/K5/1469Z9wYkqd2P5btK/Hi+pUOpuJo39cHmXaNJrmVgYwTxo4n3VAB7DQWsm\nOXYOz5wOGLl6Aj5eR8aJ0GXGz1zS2iXsWwFjakS2WfLC25HE2t6XI2pQgNG0HyEY\noS90ddxMW51dsm5fUDy9AMGitsgHFjzibI7UbHr4i3srXk08E4Br8N7Sz0YAxjhC\nPANw4A8FaB9Q17CpGZI47CRLJSr4kzxk7sIB39FoqPYd5gtfL6NugWt/x2oRQ1xM\nNVa5QQDWRbiVxYoWkBmXwAWLYir/eRiEB195/CbzTDCEtqQBaId9jTFONPjkm0TZ\nryomaCABk93+GUYgeZzb/ovJ4QS6wxXTzq+ZDxHCTPF+dksaqD/CW6jmCJvEZqPy\nNhgQ96lPYc7ejqY9yET/4CtYMNPupjVbfepeLzE3n/wnTLKlR3n9V0dIhCjcrlJl\nm7WCAJy9nxkx0xmfUFzhFweDOCDsxfsdJ8GnOcFD2b0hyEIDnC+OSVF2G8MCwfn9\n0yfY/6Vg8dmStmLCpfmlndG9aFhxQkuXTBn/5Ojy/mJaT2mt8Idr2m3pxVSpGL1h\nqfCsCW+4rC3XQhQlmfaBc0205o3G8jVadKn/1yNrraM/BOBGESALCWJZTca/Z3+z\nzTYbjxDvBHjVJOwXrqfjyQ==\n-----END CERTIFICATE-----";
7
+ /** GlobalSign Root R46 */
8
+ export declare const GLOBALSIGN_ROOT_R46 = "-----BEGIN CERTIFICATE-----\nMIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA\nMEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD\nVQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy\nMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt\nc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ\nOaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG\nvGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud\n316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo\n0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE\ny132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF\nzXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE\n+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN\nI/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs\nx2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa\nByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC\n4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4\n7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg\nJuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti\n2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk\npnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF\nFRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt\nrWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk\nZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5\nu+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP\n4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6\nN3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3\nvouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6\n-----END CERTIFICATE-----";
5
9
  export declare const CHUVSU_CA_CERTS: string[];
@@ -49,7 +49,74 @@ mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs
49
49
  Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH
50
50
  WD9f
51
51
  -----END CERTIFICATE-----`;
52
+ /** GlobalSign GCC R46 DV TLS CA 2025 (intermediate) */
53
+ export const GLOBALSIGN_GCC_R46_DV_TLS_CA_2025 = `-----BEGIN CERTIFICATE-----
54
+ MIIFfDCCA2SgAwIBAgIRAIRDWIEh21qyhJn/roBnC6QwDQYJKoZIhvcNAQELBQAw
55
+ RjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNV
56
+ BAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwHhcNMjUwOTE3MDI1NTQ0WhcNMjkwNjIz
57
+ MDAwMDAwWjBUMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1z
58
+ YTEqMCgGA1UEAxMhR2xvYmFsU2lnbiBHQ0MgUjQ2IERWIFRMUyBDQSAyMDI1MIIB
59
+ IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApT+6pung92y3k3sp2FQMmKv2
60
+ OqcOBNr9uk8IJUf/EyIHKfYRcIdgRuUu5T2PxIsvGYCAHQ3uSG/p6E1a16aMu9LV
61
+ EG9DMXcW+rx6Why5IDOIOvRHjfPyQzAGurD4V/e71U4fW81n7pKIDtcWDFeSXoai
62
+ t1cemN088vJH/L4iCSeQQ1qWsmo9VV4F8jrwWJEjOSrLLYBGHvvuteMlXzpjX2A7
63
+ pKcjOooEYU78ygS2K0YOdyN7W2hIUvScVv5yG3sA/q4/tWXY1kyUC/Bb70IgL0/n
64
+ wAYMia+OgDo5RPb+U8RUsBN4QH9qxgM75nBiOlqh/aTrrMYfYrbWOR3vioZQDQID
65
+ AQABo4IBVTCCAVEwDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMB
66
+ MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFAhp6X+lq/TDYeXNjntH74sp
67
+ ZI3sMB8GA1UdIwQYMBaAFANcq3OBh6jMsKbVlOI2lkn/BZksMHsGCCsGAQUFBwEB
68
+ BG8wbTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuZ2xvYmFsc2lnbi5jb20vcm9v
69
+ dHI0NjA7BggrBgEFBQcwAoYvaHR0cDovL3NlY3VyZS5nbG9iYWxzaWduLmNvbS9j
70
+ YWNlcnQvcm9vdHI0Ni5jcnQwNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
71
+ bG9iYWxzaWduLmNvbS9yb290cjQ2LmNybDAhBgNVHSAEGjAYMAgGBmeBDAECATAM
72
+ BgorBgEEAaAyCgEDMA0GCSqGSIb3DQEBCwUAA4ICAQAqoA/ThPjFFFZKSDkoKTKx
73
+ KA/K5/1469Z9wYkqd2P5btK/Hi+pUOpuJo39cHmXaNJrmVgYwTxo4n3VAB7DQWsm
74
+ OXYOz5wOGLl6Aj5eR8aJ0GXGz1zS2iXsWwFjakS2WfLC25HE2t6XI2pQgNG0HyEY
75
+ oS90ddxMW51dsm5fUDy9AMGitsgHFjzibI7UbHr4i3srXk08E4Br8N7Sz0YAxjhC
76
+ PANw4A8FaB9Q17CpGZI47CRLJSr4kzxk7sIB39FoqPYd5gtfL6NugWt/x2oRQ1xM
77
+ NVa5QQDWRbiVxYoWkBmXwAWLYir/eRiEB195/CbzTDCEtqQBaId9jTFONPjkm0TZ
78
+ ryomaCABk93+GUYgeZzb/ovJ4QS6wxXTzq+ZDxHCTPF+dksaqD/CW6jmCJvEZqPy
79
+ NhgQ96lPYc7ejqY9yET/4CtYMNPupjVbfepeLzE3n/wnTLKlR3n9V0dIhCjcrlJl
80
+ m7WCAJy9nxkx0xmfUFzhFweDOCDsxfsdJ8GnOcFD2b0hyEIDnC+OSVF2G8MCwfn9
81
+ 0yfY/6Vg8dmStmLCpfmlndG9aFhxQkuXTBn/5Ojy/mJaT2mt8Idr2m3pxVSpGL1h
82
+ qfCsCW+4rC3XQhQlmfaBc0205o3G8jVadKn/1yNrraM/BOBGESALCWJZTca/Z3+z
83
+ zTYbjxDvBHjVJOwXrqfjyQ==
84
+ -----END CERTIFICATE-----`;
85
+ /** GlobalSign Root R46 */
86
+ export const GLOBALSIGN_ROOT_R46 = `-----BEGIN CERTIFICATE-----
87
+ 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
116
+ -----END CERTIFICATE-----`;
52
117
  export const CHUVSU_CA_CERTS = [
53
118
  GLOBALSIGN_GCC_R3_DV_TLS_CA_2020,
54
119
  GLOBALSIGN_ROOT_R3,
120
+ GLOBALSIGN_GCC_R46_DV_TLS_CA_2025,
121
+ GLOBALSIGN_ROOT_R46,
55
122
  ];
package/dist/tt/client.js CHANGED
@@ -122,12 +122,23 @@ export class TimetableClient {
122
122
  await this.ensureRepository();
123
123
  for (let attempt = 0; attempt < 4; attempt++) {
124
124
  const expectedRevision = this._repository.revision;
125
- const result = mutate(this._repository);
126
- if (this._repository.revision === expectedRevision)
125
+ const mutation = this._repository.capturePatch(() => mutate(this._repository));
126
+ const result = mutation.result;
127
+ if (!mutation.patch)
127
128
  return result;
128
129
  if (!this.repositoryAdapter)
129
130
  return result;
130
- if (await this.repositoryAdapter.compareAndSet(expectedRevision, this._repository.export())) {
131
+ let saved;
132
+ if (this.repositoryAdapter.compareAndSetPatch) {
133
+ saved = await this.repositoryAdapter.compareAndSetPatch(expectedRevision, mutation.patch);
134
+ }
135
+ else if (this.repositoryAdapter.compareAndSet) {
136
+ saved = await this.repositoryAdapter.compareAndSet(expectedRevision, this._repository.export());
137
+ }
138
+ else {
139
+ throw new Error("Timetable repository adapter cannot persist changes");
140
+ }
141
+ if (saved) {
131
142
  return result;
132
143
  }
133
144
  const latest = await this.repositoryAdapter.load();
@@ -1,8 +1,8 @@
1
1
  import type { GroupRef, RoomRef, TeacherRef, TimetableDirectorySnapshot } from "./types.js";
2
2
  export declare class TimetableDirectory {
3
- private groups;
4
- private teachers;
5
- private rooms;
3
+ private readonly groups;
4
+ private readonly teachers;
5
+ private readonly rooms;
6
6
  constructor(snapshot?: TimetableDirectorySnapshot);
7
7
  rememberGroups(values: GroupRef[]): boolean;
8
8
  rememberTeachers(values: TeacherRef[]): boolean;
@@ -1,4 +1,4 @@
1
- import { mergeEntityRefs, normalizeScheduleText, } from "./normalize.js";
1
+ import { entityKey, mergeEntityRefs, normalizeScheduleText, } from "./normalize.js";
2
2
  function personKey(name) {
3
3
  const words = normalizeScheduleText(name)
4
4
  .replace(/[.,]/gu, " ")
@@ -11,106 +11,167 @@ function personKey(name) {
11
11
  .join("");
12
12
  return `${surname}|${initials}`;
13
13
  }
14
- function exactKey(name) {
15
- return normalizeScheduleText(name);
16
- }
17
14
  function validateRefs(values, label) {
18
15
  for (const value of values) {
19
16
  if (!value.name.trim())
20
17
  throw new TypeError(`${label} name is empty`);
21
- if (value.id != null &&
22
- (!Number.isInteger(value.id) || value.id < 1)) {
18
+ if (value.id != null && (!Number.isInteger(value.id) || value.id < 1)) {
23
19
  throw new RangeError(`${label} ID must be a positive integer`);
24
20
  }
25
21
  }
26
22
  }
27
- function mergeByIdAndName(current, incoming) {
28
- const values = mergeEntityRefs([...current, ...incoming]);
29
- const before = JSON.stringify(current);
30
- const after = JSON.stringify(values);
31
- return { values, changed: before !== after };
32
- }
33
- function mergeTeachersByIdentity(current, incoming) {
34
- const merged = mergeEntityRefs([...current, ...incoming]);
35
- const identified = merged.filter((value) => value.id != null);
36
- const unresolved = [];
37
- for (const alias of merged.filter((value) => value.id == null)) {
38
- const matches = identified
39
- .map((value, index) => ({ value, index }))
40
- .filter(({ value }) => personKey(value.name) === personKey(alias.name));
41
- const ids = new Set(matches.map(({ value }) => value.id));
42
- if (ids.size === 1) {
43
- const match = matches[0];
44
- identified[match.index] = { ...alias, ...match.value };
23
+ class EntityIndex {
24
+ teacherIdentity;
25
+ records = new Set();
26
+ byId = new Map();
27
+ byName = new Map();
28
+ byPerson = new Map();
29
+ constructor(values, teacherIdentity = false) {
30
+ this.teacherIdentity = teacherIdentity;
31
+ for (const value of values)
32
+ this.rememberOne(structuredClone(value));
33
+ }
34
+ remember(values) {
35
+ let changed = false;
36
+ for (const value of values) {
37
+ if (this.rememberOne(structuredClone(value)))
38
+ changed = true;
39
+ }
40
+ return changed;
41
+ }
42
+ resolve(value) {
43
+ const exact = value.id == null
44
+ ? this.byName.get(normalizeScheduleText(value.name))
45
+ : this.byId.get(value.id);
46
+ if (exact)
47
+ return mergeEntityRefs([value, exact.value])[0];
48
+ if (!this.teacherIdentity)
49
+ return structuredClone(value);
50
+ const candidates = this.byPerson.get(personKey(value.name)) ?? new Set();
51
+ const identified = [...candidates].filter((candidate) => candidate.value.id != null);
52
+ const ids = new Set(identified.map((candidate) => candidate.value.id));
53
+ return ids.size === 1
54
+ ? mergeEntityRefs([value, identified[0].value])[0]
55
+ : structuredClone(value);
56
+ }
57
+ export() {
58
+ return [...this.records]
59
+ .map((record) => structuredClone(record.value))
60
+ .sort((a, b) => entityKey(a).localeCompare(entityKey(b)));
61
+ }
62
+ rememberOne(value) {
63
+ const name = normalizeScheduleText(value.name);
64
+ const person = this.teacherIdentity ? personKey(value.name) : "";
65
+ let record = value.id == null ? undefined : this.byId.get(value.id);
66
+ record ??= this.byName.get(name);
67
+ if (!record && this.teacherIdentity) {
68
+ const identified = [...(this.byPerson.get(person) ?? [])]
69
+ .filter((candidate) => candidate.value.id != null);
70
+ const ids = new Set(identified.map((candidate) => candidate.value.id));
71
+ if (ids.size === 1)
72
+ record = identified[0];
45
73
  }
46
- else {
47
- unresolved.push(alias);
74
+ if (!record) {
75
+ record = { value, ids: new Set(), names: new Set(), people: new Set() };
76
+ this.records.add(record);
77
+ this.index(record, value);
78
+ if (this.teacherIdentity && value.id != null)
79
+ this.mergeTeacherAliases(record, person);
80
+ return true;
48
81
  }
82
+ const before = JSON.stringify(record.value);
83
+ record.value = mergeEntityRefs([record.value, value])[0];
84
+ this.index(record, value);
85
+ this.index(record, record.value);
86
+ if (this.teacherIdentity && record.value.id != null)
87
+ this.mergeTeacherAliases(record, person);
88
+ return before !== JSON.stringify(record.value);
89
+ }
90
+ index(record, value) {
91
+ if (value.id != null) {
92
+ record.ids.add(value.id);
93
+ this.byId.set(value.id, record);
94
+ }
95
+ const name = normalizeScheduleText(value.name);
96
+ if (name) {
97
+ record.names.add(name);
98
+ this.byName.set(name, record);
99
+ }
100
+ if (this.teacherIdentity) {
101
+ const person = personKey(value.name);
102
+ record.people.add(person);
103
+ let records = this.byPerson.get(person);
104
+ if (!records) {
105
+ records = new Set();
106
+ this.byPerson.set(person, records);
107
+ }
108
+ records.add(record);
109
+ }
110
+ }
111
+ mergeTeacherAliases(target, person) {
112
+ const aliases = [...(this.byPerson.get(person) ?? [])]
113
+ .filter((candidate) => candidate !== target && candidate.value.id == null);
114
+ for (const alias of aliases)
115
+ this.mergeRecords(target, alias);
116
+ }
117
+ mergeRecords(target, source) {
118
+ target.value = mergeEntityRefs([source.value, target.value])[0];
119
+ for (const id of source.ids) {
120
+ target.ids.add(id);
121
+ this.byId.set(id, target);
122
+ }
123
+ for (const name of source.names) {
124
+ target.names.add(name);
125
+ this.byName.set(name, target);
126
+ }
127
+ for (const person of source.people) {
128
+ target.people.add(person);
129
+ const records = this.byPerson.get(person);
130
+ records?.delete(source);
131
+ records?.add(target);
132
+ }
133
+ this.records.delete(source);
49
134
  }
50
- const values = mergeEntityRefs([...identified, ...unresolved]);
51
- return {
52
- values,
53
- changed: JSON.stringify(current) !== JSON.stringify(values),
54
- };
55
- }
56
- function resolveUnique(value, known, key) {
57
- if (value.id != null) {
58
- const byId = known.find((candidate) => candidate.id === value.id);
59
- return byId ? mergeEntityRefs([value, byId])[0] : value;
60
- }
61
- const lookup = key(value.name);
62
- const candidates = known.filter((candidate) => candidate.id != null && key(candidate.name) === lookup);
63
- const ids = new Set(candidates.map((candidate) => candidate.id));
64
- if (ids.size !== 1)
65
- return value;
66
- return mergeEntityRefs([value, candidates[0]])[0];
67
135
  }
68
136
  export class TimetableDirectory {
69
- groups = [];
70
- teachers = [];
71
- rooms = [];
137
+ groups;
138
+ teachers;
139
+ rooms;
72
140
  constructor(snapshot) {
73
- if (snapshot) {
74
- validateRefs(snapshot.groups, "Group");
75
- validateRefs(snapshot.teachers, "Teacher");
76
- validateRefs(snapshot.rooms, "Room");
77
- this.groups = mergeEntityRefs(structuredClone(snapshot.groups));
78
- this.teachers = mergeTeachersByIdentity([], structuredClone(snapshot.teachers)).values;
79
- this.rooms = mergeEntityRefs(structuredClone(snapshot.rooms));
80
- }
141
+ const initial = snapshot ?? { groups: [], teachers: [], rooms: [] };
142
+ validateRefs(initial.groups, "Group");
143
+ validateRefs(initial.teachers, "Teacher");
144
+ validateRefs(initial.rooms, "Room");
145
+ this.groups = new EntityIndex(initial.groups);
146
+ this.teachers = new EntityIndex(initial.teachers, true);
147
+ this.rooms = new EntityIndex(initial.rooms);
81
148
  }
82
149
  rememberGroups(values) {
83
150
  validateRefs(values, "Group");
84
- const result = mergeByIdAndName(this.groups, values);
85
- this.groups = result.values;
86
- return result.changed;
151
+ return this.groups.remember(values);
87
152
  }
88
153
  rememberTeachers(values) {
89
154
  validateRefs(values, "Teacher");
90
- const result = mergeTeachersByIdentity(this.teachers, values);
91
- this.teachers = result.values;
92
- return result.changed;
155
+ return this.teachers.remember(values);
93
156
  }
94
157
  rememberRooms(values) {
95
158
  validateRefs(values, "Room");
96
- const result = mergeByIdAndName(this.rooms, values);
97
- this.rooms = result.values;
98
- return result.changed;
159
+ return this.rooms.remember(values);
99
160
  }
100
161
  resolveGroup(value) {
101
- return resolveUnique(value, this.groups, exactKey);
162
+ return this.groups.resolve(value);
102
163
  }
103
164
  resolveTeacher(value) {
104
- return resolveUnique(value, this.teachers, personKey);
165
+ return this.teachers.resolve(value);
105
166
  }
106
167
  resolveRoom(value) {
107
- return resolveUnique(value, this.rooms, exactKey);
168
+ return this.rooms.resolve(value);
108
169
  }
109
170
  export() {
110
171
  return {
111
- groups: structuredClone(this.groups),
112
- teachers: structuredClone(this.teachers),
113
- rooms: structuredClone(this.rooms),
172
+ groups: this.groups.export(),
173
+ teachers: this.teachers.export(),
174
+ rooms: this.rooms.export(),
114
175
  };
115
176
  }
116
177
  }
@@ -6,4 +6,4 @@ export { Schedule } from "./schedule.js";
6
6
  export { MaterializedSchedule, materializeSchedule, materializeScheduleSnapshot, } from "./materialized-schedule.js";
7
7
  export type { ScheduleQueryOptions, ScheduleOptions, ScheduleWeekdayOptions, } from "./schedule.js";
8
8
  export type { MaterializeScheduleOptions, MaterializedScheduleSnapshot, SerializedLessonOccurrence, SerializedLessonSourceRef, } from "./materialized-schedule.js";
9
- export type { GroupAttendance, GroupRef, IngestResult, LessonId, LessonIdGenerator, LessonOccurrence, LessonRecurrence, LessonSeries, LessonSeriesId, RelationCompleteness, RelationSet, LessonSourceRef, LessonStatus, LessonSubstitution, LessonTransfer, NamedEntityRef, OccurrenceObservation, RoomRef, ScheduleObservation, ScheduleOwner, ScheduleSourceSnapshot, SeriesObservation, TeacherRef, TimetableDirectorySnapshot, TimetableRepositoryAdapter, TimetableRepositorySnapshot, } from "./types.js";
9
+ export type { GroupAttendance, GroupRef, IngestResult, LessonId, LessonIdGenerator, LessonOccurrence, LessonRecurrence, LessonSeries, LessonSeriesId, RelationCompleteness, RelationSet, LessonSourceRef, LessonStatus, LessonSubstitution, LessonTransfer, NamedEntityRef, OccurrenceObservation, RoomRef, ScheduleObservation, ScheduleOwner, ScheduleSourceSnapshot, SeriesObservation, TeacherRef, TimetableDirectorySnapshot, TimetableRepositoryAdapter, TimetableRepositoryPatch, TimetableRepositorySnapshot, } from "./types.js";
@@ -1,6 +1,6 @@
1
1
  import type { AcademicPeriod } from "../../common/types.js";
2
2
  import { TimetableDirectory } from "./directory.js";
3
- import type { IngestResult, LessonIdGenerator, LessonOccurrence, LessonSeries, ScheduleOwner, ScheduleSourceSnapshot, TimetableRepositoryAdapter, TimetableRepositorySnapshot } from "./types.js";
3
+ import type { IngestResult, LessonIdGenerator, LessonOccurrence, LessonSeries, ScheduleOwner, ScheduleSourceSnapshot, TimetableRepositoryAdapter, TimetableRepositoryPatch, TimetableRepositorySnapshot } from "./types.js";
4
4
  export declare class TimetableRepository {
5
5
  private readonly idGenerator;
6
6
  private _directory;
@@ -10,6 +10,7 @@ export declare class TimetableRepository {
10
10
  private readonly occurrenceRecords;
11
11
  private readonly recordIdsByBucket;
12
12
  private readonly recordIdsByOwner;
13
+ private patchTracker;
13
14
  private _revision;
14
15
  constructor(opts?: {
15
16
  idGenerator?: LessonIdGenerator;
@@ -20,6 +21,10 @@ export declare class TimetableRepository {
20
21
  rememberGroups(values: Parameters<TimetableDirectory["rememberGroups"]>[0]): void;
21
22
  rememberTeachers(values: Parameters<TimetableDirectory["rememberTeachers"]>[0]): void;
22
23
  rememberRooms(values: Parameters<TimetableDirectory["rememberRooms"]>[0]): void;
24
+ capturePatch<T>(mutate: () => T): {
25
+ result: T;
26
+ patch: TimetableRepositoryPatch | null;
27
+ };
23
28
  ingest(snapshot: ScheduleSourceSnapshot): IngestResult;
24
29
  getSeries(opts?: {
25
30
  owner?: ScheduleOwner;
@@ -32,11 +37,11 @@ export declare class TimetableRepository {
32
37
  periods?: readonly AcademicPeriod[];
33
38
  }): LessonOccurrence[];
34
39
  export(): TimetableRepositorySnapshot;
40
+ private buildPatch;
35
41
  replaceSnapshot(snapshot: TimetableRepositorySnapshot): void;
36
42
  private restore;
37
43
  private validateSource;
38
44
  private rememberSourceEntities;
39
- private pruneEmptyRecords;
40
45
  private removeClaim;
41
46
  private addClaim;
42
47
  private findPriorTarget;
@@ -1,7 +1,7 @@
1
1
  import { isLocalDate } from "../utils/date.js";
2
2
  import { TimetableDirectory } from "./directory.js";
3
3
  import { RandomLessonIdGenerator } from "./ids.js";
4
- import { entityKey, mergeGroups, mergeRooms, mergeTeachers, normalizeScheduleText, } from "./normalize.js";
4
+ import { entityKey, mergeEntityRefs, mergeGroups, mergeRooms, mergeTeachers, normalizeScheduleText, } from "./normalize.js";
5
5
  function claimKey(sourceKey, observationKey) {
6
6
  return `${sourceKey}\u0000${observationKey}`;
7
7
  }
@@ -282,6 +282,7 @@ export class TimetableRepository {
282
282
  occurrenceRecords = new Map();
283
283
  recordIdsByBucket = new Map();
284
284
  recordIdsByOwner = new Map();
285
+ patchTracker = null;
285
286
  _revision = 0;
286
287
  constructor(opts) {
287
288
  this.idGenerator = opts?.idGenerator ?? new RandomLessonIdGenerator();
@@ -296,24 +297,54 @@ export class TimetableRepository {
296
297
  return this._revision;
297
298
  }
298
299
  rememberGroups(values) {
299
- if (this.directory.rememberGroups(values))
300
+ if (this.directory.rememberGroups(values)) {
301
+ this.patchTracker?.groups.push(...structuredClone(values));
300
302
  this._revision++;
303
+ }
301
304
  }
302
305
  rememberTeachers(values) {
303
- if (this.directory.rememberTeachers(values))
306
+ if (this.directory.rememberTeachers(values)) {
307
+ this.patchTracker?.teachers.push(...structuredClone(values));
304
308
  this._revision++;
309
+ }
305
310
  }
306
311
  rememberRooms(values) {
307
- if (this.directory.rememberRooms(values))
312
+ if (this.directory.rememberRooms(values)) {
313
+ this.patchTracker?.rooms.push(...structuredClone(values));
308
314
  this._revision++;
315
+ }
316
+ }
317
+ capturePatch(mutate) {
318
+ if (this.patchTracker)
319
+ throw new Error("Nested timetable repository mutation");
320
+ const expectedRevision = this._revision;
321
+ const tracker = {
322
+ sourceKeys: new Set(),
323
+ groups: [],
324
+ teachers: [],
325
+ rooms: [],
326
+ };
327
+ this.patchTracker = tracker;
328
+ try {
329
+ const result = mutate();
330
+ return {
331
+ result,
332
+ patch: this._revision === expectedRevision ? null : this.buildPatch(tracker),
333
+ };
334
+ }
335
+ finally {
336
+ this.patchTracker = null;
337
+ }
309
338
  }
310
339
  ingest(snapshot) {
311
340
  const incoming = structuredClone(snapshot);
312
341
  this.validateSource(incoming);
313
- this.rememberSourceEntities(incoming);
342
+ const directoryChanged = this.rememberSourceEntities(incoming);
314
343
  const prior = this.sources.get(incoming.sourceKey);
315
344
  if (prior && sameSourceContent(prior, incoming)) {
316
345
  const links = incoming.observations.map((observation) => this.links.get(sourceObservationKey(incoming.sourceKey, observation.key)));
346
+ if (directoryChanged)
347
+ this._revision++;
317
348
  return {
318
349
  revision: this._revision,
319
350
  seriesIds: links.flatMap((link) => link?.kind === "series" ? [link.id] : []),
@@ -331,6 +362,7 @@ export class TimetableRepository {
331
362
  }
332
363
  }
333
364
  this.sources.set(incoming.sourceKey, incoming);
365
+ this.patchTracker?.sourceKeys.add(incoming.sourceKey);
334
366
  const usedTargets = new Set();
335
367
  const seriesIds = [];
336
368
  const lessonIds = [];
@@ -378,7 +410,6 @@ export class TimetableRepository {
378
410
  }
379
411
  }
380
412
  }
381
- this.pruneEmptyRecords();
382
413
  this._revision++;
383
414
  return {
384
415
  revision: this._revision,
@@ -453,6 +484,32 @@ export class TimetableRepository {
453
484
  a.observationKey.localeCompare(b.observationKey)),
454
485
  };
455
486
  }
487
+ buildPatch(tracker) {
488
+ return {
489
+ schemaVersion: 5,
490
+ revision: this._revision,
491
+ directoryUpserts: {
492
+ groups: mergeEntityRefs(tracker.groups),
493
+ teachers: mergeTeachers(tracker.teachers),
494
+ rooms: mergeRooms(tracker.rooms),
495
+ },
496
+ sourceReplacements: [...tracker.sourceKeys].sort().flatMap((sourceKey) => {
497
+ const source = this.sources.get(sourceKey);
498
+ if (!source)
499
+ return [];
500
+ return [{
501
+ source: serializeSource(source),
502
+ links: source.observations.map((observation) => {
503
+ const link = this.links.get(sourceObservationKey(sourceKey, observation.key));
504
+ if (!link) {
505
+ throw new Error(`Missing timetable link: ${sourceKey}/${observation.key}`);
506
+ }
507
+ return { observationKey: observation.key, ...link };
508
+ }),
509
+ }];
510
+ }),
511
+ };
512
+ }
456
513
  replaceSnapshot(snapshot) {
457
514
  this.restore(snapshot);
458
515
  }
@@ -623,23 +680,16 @@ export class TimetableRepository {
623
680
  rooms.push(...(substitution.rooms ?? []));
624
681
  }
625
682
  }
626
- this.directory.rememberGroups(groups);
627
- this.directory.rememberTeachers(teachers);
628
- this.directory.rememberRooms(rooms);
629
- }
630
- pruneEmptyRecords() {
631
- for (const [id, record] of this.seriesRecords) {
632
- if (record.claims.size === 0) {
633
- this.removeRecordIndexes(record);
634
- this.seriesRecords.delete(id);
635
- }
636
- }
637
- for (const [id, record] of this.occurrenceRecords) {
638
- if (record.claims.size === 0) {
639
- this.removeRecordIndexes(record);
640
- this.occurrenceRecords.delete(id);
641
- }
642
- }
683
+ const groupsChanged = this.directory.rememberGroups(groups);
684
+ const teachersChanged = this.directory.rememberTeachers(teachers);
685
+ const roomsChanged = this.directory.rememberRooms(rooms);
686
+ if (groupsChanged)
687
+ this.patchTracker?.groups.push(...structuredClone(groups));
688
+ if (teachersChanged)
689
+ this.patchTracker?.teachers.push(...structuredClone(teachers));
690
+ if (roomsChanged)
691
+ this.patchTracker?.rooms.push(...structuredClone(rooms));
692
+ return groupsChanged || teachersChanged || roomsChanged;
643
693
  }
644
694
  removeClaim(sourceKey, observationKey) {
645
695
  const link = this.links.get(sourceObservationKey(sourceKey, observationKey));
@@ -651,6 +701,14 @@ export class TimetableRepository {
651
701
  if (!record)
652
702
  return;
653
703
  record.claims.delete(claimKey(sourceKey, observationKey));
704
+ if (record.claims.size === 0) {
705
+ this.removeRecordIndexes(record);
706
+ if (link.kind === "series")
707
+ this.seriesRecords.delete(link.id);
708
+ else
709
+ this.occurrenceRecords.delete(link.id);
710
+ return;
711
+ }
654
712
  this.rebuildRecordOwnerIndex(record);
655
713
  }
656
714
  addClaim(link, source, observation) {
@@ -158,6 +158,21 @@ export interface TimetableRepositorySnapshot {
158
158
  id: string;
159
159
  }>;
160
160
  }
161
+ export interface TimetableRepositoryPatch {
162
+ schemaVersion: 5;
163
+ revision: number;
164
+ /** Entity rows changed by this mutation. Merge them into persistent directory storage. */
165
+ directoryUpserts: TimetableDirectorySnapshot;
166
+ /** Complete replacements for touched sources. Untouched source rows stay unchanged. */
167
+ sourceReplacements: Array<{
168
+ source: SerializedScheduleSourceSnapshot;
169
+ links: Array<{
170
+ observationKey: string;
171
+ kind: "series" | "occurrence";
172
+ id: string;
173
+ }>;
174
+ }>;
175
+ }
161
176
  export interface TimetableDirectorySnapshot {
162
177
  groups: GroupRef[];
163
178
  teachers: TeacherRef[];
@@ -165,7 +180,10 @@ export interface TimetableDirectorySnapshot {
165
180
  }
166
181
  export interface TimetableRepositoryAdapter {
167
182
  load(): Promise<TimetableRepositorySnapshot | null>;
168
- compareAndSet(expectedRevision: number, snapshot: TimetableRepositorySnapshot): Promise<boolean>;
183
+ /** Legacy full-snapshot CAS fallback. Prefer compareAndSetPatch for persistent storage. */
184
+ compareAndSet?(expectedRevision: number, snapshot: TimetableRepositorySnapshot): Promise<boolean>;
185
+ /** Incremental CAS: persist only supplied entity upserts and source replacements. */
186
+ compareAndSetPatch?(expectedRevision: number, patch: TimetableRepositoryPatch): Promise<boolean>;
169
187
  clear?(): Promise<void>;
170
188
  }
171
189
  export interface LessonIdGenerator {
@@ -117,7 +117,11 @@ ID серии создается генератором и хранится в
117
117
  3. Производные представления запросов, инвалидируемые ревизией.
118
118
 
119
119
  Снимок репозитория имеет `schemaVersion: 5`. Адаптер постоянного хранилища
120
- использует compare-and-set по ревизии.
120
+ использует compare-and-set по ревизии. Предпочтительный метод
121
+ `compareAndSetPatch` атомарно применяет только измененные строки справочника и
122
+ полностью заменяет затронутые источники со связями. Обычная мутация не экспортирует
123
+ и не обходит весь репозиторий; полный снимок остается форматом загрузки, переноса и
124
+ резервного копирования.
121
125
 
122
126
  ## Запросы
123
127
 
@@ -281,11 +281,16 @@ const snapshot = await client.exportRepository();
281
281
  вместе со всеми когда-либо загруженными расписаниями, а восстановление требует
282
282
  повторной агрегации связей.
283
283
 
284
- Начиная с 5.1.2 поиск совпадающих занятий и выборка расписания владельца используют
285
- внутренние индексы, одинаковые повторные снимки не меняют ревизию, а периоды одного
286
- запроса сохраняются одной транзакцией адаптера. Это исключает полный обход
287
- репозитория из обычного пути запроса; `exportRepository()` по-прежнему имеет цену,
288
- пропорциональную размеру полного снимка, и не должен вызываться на клиентском пути.
284
+ Начиная с 5.2.0 поиск совпадающих занятий, справочник сущностей и удаление пустых
285
+ канонических записей используют локальные индексы. Одинаковые повторные снимки не
286
+ меняют ревизию. `compareAndSetPatch` получает только `directoryUpserts` и
287
+ `sourceReplacements`, затронутые одной мутацией. Поэтому размер записи и обычное
288
+ время обновления не растут вместе со всем репозиторием. Старый `compareAndSet`
289
+ поддерживается как fallback, но сериализует полный снимок.
290
+
291
+ `exportRepository()` по-прежнему имеет цену, пропорциональную размеру полного
292
+ снимка. Используйте его для экспорта, резервной копии или миграции, не на пути
293
+ каждого запроса.
289
294
 
290
295
  Для API, SSR и офлайн-кеша материализуйте только готовое расписание владельца:
291
296
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chuvsu-js",
3
- "version": "5.1.2",
3
+ "version": "5.2.1",
4
4
  "description": "Библиотека для расписания и личного кабинета ЧГУ им. И. Н. Ульянова",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",