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 +5 -1
- package/dist/common/certs.d.ts +4 -0
- package/dist/common/certs.js +67 -0
- package/dist/tt/client.js +14 -3
- package/dist/tt/domain/directory.d.ts +3 -3
- package/dist/tt/domain/directory.js +130 -69
- package/dist/tt/domain/index.d.ts +1 -1
- package/dist/tt/domain/repository.d.ts +7 -2
- package/dist/tt/domain/repository.js +81 -23
- package/dist/tt/domain/types.d.ts +19 -1
- package/docs/v5-architecture.md +5 -1
- package/docs/v5-migration.md +10 -5
- package/package.json +1 -1
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();
|
package/dist/common/certs.d.ts
CHANGED
|
@@ -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[];
|
package/dist/common/certs.js
CHANGED
|
@@ -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
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
this.rooms = result.values;
|
|
98
|
-
return result.changed;
|
|
159
|
+
return this.rooms.remember(values);
|
|
99
160
|
}
|
|
100
161
|
resolveGroup(value) {
|
|
101
|
-
return
|
|
162
|
+
return this.groups.resolve(value);
|
|
102
163
|
}
|
|
103
164
|
resolveTeacher(value) {
|
|
104
|
-
return
|
|
165
|
+
return this.teachers.resolve(value);
|
|
105
166
|
}
|
|
106
167
|
resolveRoom(value) {
|
|
107
|
-
return
|
|
168
|
+
return this.rooms.resolve(value);
|
|
108
169
|
}
|
|
109
170
|
export() {
|
|
110
171
|
return {
|
|
111
|
-
groups:
|
|
112
|
-
teachers:
|
|
113
|
-
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
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 {
|
package/docs/v5-architecture.md
CHANGED
|
@@ -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
|
|
package/docs/v5-migration.md
CHANGED
|
@@ -281,11 +281,16 @@ const snapshot = await client.exportRepository();
|
|
|
281
281
|
вместе со всеми когда-либо загруженными расписаниями, а восстановление требует
|
|
282
282
|
повторной агрегации связей.
|
|
283
283
|
|
|
284
|
-
Начиная с 5.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|