@neottia/repository-store 0.2.0 → 0.3.0

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 (55) hide show
  1. package/README.md +8 -5
  2. package/binding.gyp +12 -2
  3. package/dist/files.d.ts.map +1 -1
  4. package/dist/files.js +2 -2
  5. package/dist/files.js.map +1 -1
  6. package/dist/internal/filesystem.d.ts +3 -1
  7. package/dist/internal/filesystem.d.ts.map +1 -1
  8. package/dist/internal/filesystem.js +95 -13
  9. package/dist/internal/filesystem.js.map +1 -1
  10. package/dist/internal/model.d.ts +1 -0
  11. package/dist/internal/model.d.ts.map +1 -1
  12. package/dist/internal/model.js.map +1 -1
  13. package/dist/internal/platform-backend.d.ts +24 -0
  14. package/dist/internal/platform-backend.d.ts.map +1 -0
  15. package/dist/internal/platform-backend.js +120 -0
  16. package/dist/internal/platform-backend.js.map +1 -0
  17. package/dist/internal/publication.d.ts.map +1 -1
  18. package/dist/internal/publication.js +28 -16
  19. package/dist/internal/publication.js.map +1 -1
  20. package/dist/lease.d.ts +3 -0
  21. package/dist/lease.d.ts.map +1 -1
  22. package/dist/lease.js +241 -52
  23. package/dist/lease.js.map +1 -1
  24. package/dist/paths.d.ts.map +1 -1
  25. package/dist/paths.js +4 -1
  26. package/dist/paths.js.map +1 -1
  27. package/dist/sqlite/adapter.d.ts +1 -0
  28. package/dist/sqlite/adapter.d.ts.map +1 -1
  29. package/dist/sqlite/adapter.js +9 -0
  30. package/dist/sqlite/adapter.js.map +1 -1
  31. package/dist/sqlite/cache-publication.d.ts.map +1 -1
  32. package/dist/sqlite/cache-publication.js +2 -2
  33. package/dist/sqlite/cache-publication.js.map +1 -1
  34. package/dist/sqlite/cache.d.ts.map +1 -1
  35. package/dist/sqlite/cache.js +1 -2
  36. package/dist/sqlite/cache.js.map +1 -1
  37. package/dist/transaction/apply.js +2 -2
  38. package/dist/transaction/apply.js.map +1 -1
  39. package/dist/transaction/journal.js +2 -2
  40. package/dist/transaction/journal.js.map +1 -1
  41. package/dist/transaction/recovery.js +2 -2
  42. package/dist/transaction/recovery.js.map +1 -1
  43. package/native/{rename_noreplace.cc → backend.cc} +47 -49
  44. package/native/freebsd_backend.cc +83 -0
  45. package/native/linux_backend.cc +45 -0
  46. package/native/macos_backend.cc +84 -0
  47. package/native/platform_backend.h +17 -0
  48. package/native/unsupported_backend.cc +28 -0
  49. package/native/windows_backend.cc +179 -0
  50. package/package.json +5 -5
  51. package/scripts/install-native.mjs +2 -2
  52. package/dist/internal/native-publication.d.ts +0 -9
  53. package/dist/internal/native-publication.d.ts.map +0 -1
  54. package/dist/internal/native-publication.js +0 -65
  55. package/dist/internal/native-publication.js.map +0 -1
@@ -0,0 +1,179 @@
1
+ #include "platform_backend.h"
2
+
3
+ #define WIN32_LEAN_AND_MEAN
4
+ #ifndef NOMINMAX
5
+ #define NOMINMAX
6
+ #endif
7
+ #include <windows.h>
8
+
9
+ #include <cerrno>
10
+ #include <cstddef>
11
+ #include <cstdint>
12
+ #include <cstring>
13
+ #include <cwchar>
14
+ #include <string>
15
+ #include <vector>
16
+
17
+ namespace neottia::repository_store {
18
+ namespace {
19
+
20
+ int WindowsError(DWORD error) {
21
+ switch (error) {
22
+ case ERROR_FILE_EXISTS:
23
+ case ERROR_ALREADY_EXISTS:
24
+ return EEXIST;
25
+ case ERROR_FILE_NOT_FOUND:
26
+ case ERROR_PATH_NOT_FOUND:
27
+ return ENOENT;
28
+ case ERROR_NOT_SAME_DEVICE:
29
+ return EXDEV;
30
+ case ERROR_ACCESS_DENIED:
31
+ return EACCES;
32
+ case ERROR_SHARING_VIOLATION:
33
+ case ERROR_LOCK_VIOLATION:
34
+ return EBUSY;
35
+ case ERROR_INVALID_HANDLE:
36
+ return EBADF;
37
+ case ERROR_CALL_NOT_IMPLEMENTED:
38
+ return ENOSYS;
39
+ case ERROR_NOT_SUPPORTED:
40
+ return ENOTSUP;
41
+ case ERROR_INVALID_NAME:
42
+ case ERROR_BAD_PATHNAME:
43
+ case ERROR_INVALID_PARAMETER:
44
+ return EINVAL;
45
+ default:
46
+ return EIO;
47
+ }
48
+ }
49
+
50
+ int Utf8Path(const char* path, std::wstring* output) {
51
+ if (path == nullptr) return EINVAL;
52
+ const int required =
53
+ MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, nullptr, 0);
54
+ if (required == 0) return WindowsError(GetLastError());
55
+ std::vector<wchar_t> buffer(static_cast<std::size_t>(required));
56
+ if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1,
57
+ buffer.data(), required) == 0)
58
+ return WindowsError(GetLastError());
59
+ output->assign(buffer.data());
60
+ return 0;
61
+ }
62
+
63
+ int AbsolutePath(const char* path, std::wstring* output) {
64
+ std::wstring decoded;
65
+ const int decode_error = Utf8Path(path, &decoded);
66
+ if (decode_error != 0) return decode_error;
67
+ const DWORD required = GetFullPathNameW(decoded.c_str(), 0, nullptr, nullptr);
68
+ if (required == 0) return WindowsError(GetLastError());
69
+ std::vector<wchar_t> buffer(static_cast<std::size_t>(required));
70
+ const DWORD copied =
71
+ GetFullPathNameW(decoded.c_str(), required, buffer.data(), nullptr);
72
+ if (copied == 0 || copied >= required)
73
+ return copied == 0 ? WindowsError(GetLastError()) : EINVAL;
74
+ output->assign(buffer.data(), copied);
75
+ return 0;
76
+ }
77
+
78
+ std::wstring ExtendedPath(const std::wstring& path) {
79
+ if (path.rfind(L"\\\\?\\", 0) == 0) return path;
80
+ if (path.rfind(L"\\\\", 0) == 0) return L"\\\\?\\UNC\\" + path.substr(2);
81
+ return L"\\\\?\\" + path;
82
+ }
83
+
84
+ bool IsNtfs(const wchar_t* filesystem_name) {
85
+ return _wcsicmp(filesystem_name, L"NTFS") == 0;
86
+ }
87
+
88
+ } // namespace
89
+
90
+ const char* Platform() { return "win32"; }
91
+
92
+ int RenameNoReplace(const char* source, const char* destination) {
93
+ std::wstring source_path;
94
+ int error = AbsolutePath(source, &source_path);
95
+ if (error != 0) return error;
96
+ std::wstring destination_path;
97
+ error = AbsolutePath(destination, &destination_path);
98
+ if (error != 0) return error;
99
+
100
+ const std::wstring extended_source = ExtendedPath(source_path);
101
+ const std::wstring extended_destination = ExtendedPath(destination_path);
102
+ HANDLE handle =
103
+ CreateFileW(extended_source.c_str(), DELETE | FILE_READ_ATTRIBUTES,
104
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
105
+ nullptr, OPEN_EXISTING,
106
+ FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS |
107
+ FILE_FLAG_OPEN_REPARSE_POINT,
108
+ nullptr);
109
+ if (handle == INVALID_HANDLE_VALUE) return WindowsError(GetLastError());
110
+
111
+ FILE_ATTRIBUTE_TAG_INFO attributes = {};
112
+ if (!GetFileInformationByHandleEx(handle, FileAttributeTagInfo, &attributes,
113
+ sizeof(attributes))) {
114
+ error = WindowsError(GetLastError());
115
+ CloseHandle(handle);
116
+ return error;
117
+ }
118
+ if ((attributes.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) {
119
+ CloseHandle(handle);
120
+ return EINVAL;
121
+ }
122
+
123
+ const std::size_t name_bytes = extended_destination.size() * sizeof(wchar_t);
124
+ if (name_bytes >
125
+ static_cast<std::size_t>(MAXDWORD) - sizeof(FILE_RENAME_INFO)) {
126
+ CloseHandle(handle);
127
+ return EINVAL;
128
+ }
129
+ std::vector<std::uint8_t> storage(sizeof(FILE_RENAME_INFO) + name_bytes);
130
+ auto* rename = reinterpret_cast<FILE_RENAME_INFO*>(storage.data());
131
+ rename->Flags = 0;
132
+ rename->RootDirectory = nullptr;
133
+ rename->FileNameLength = static_cast<DWORD>(name_bytes);
134
+ std::memcpy(rename->FileName, extended_destination.data(), name_bytes);
135
+
136
+ if (!SetFileInformationByHandle(handle, FileRenameInfoEx, rename,
137
+ static_cast<DWORD>(storage.size()))) {
138
+ error = WindowsError(GetLastError());
139
+ CloseHandle(handle);
140
+ return error;
141
+ }
142
+ if (!CloseHandle(handle)) return WindowsError(GetLastError());
143
+ return 0;
144
+ }
145
+
146
+ int SupportsLocalPath(const char* path, bool* supported) {
147
+ *supported = false;
148
+ std::wstring absolute;
149
+ int error = AbsolutePath(path, &absolute);
150
+ if (error != 0) return error;
151
+
152
+ const std::wstring extended = ExtendedPath(absolute);
153
+ const DWORD attributes = GetFileAttributesW(extended.c_str());
154
+ if (attributes == INVALID_FILE_ATTRIBUTES)
155
+ return WindowsError(GetLastError());
156
+ if ((attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) return 0;
157
+
158
+ wchar_t volume_path[MAX_PATH + 1] = {};
159
+ if (!GetVolumePathNameW(absolute.c_str(), volume_path, MAX_PATH + 1))
160
+ return WindowsError(GetLastError());
161
+ if (GetDriveTypeW(volume_path) != DRIVE_FIXED) return 0;
162
+
163
+ DWORD filesystem_flags = 0;
164
+ wchar_t filesystem_name[MAX_PATH + 1] = {};
165
+ if (!GetVolumeInformationW(volume_path, nullptr, 0, nullptr, nullptr,
166
+ &filesystem_flags, filesystem_name, MAX_PATH + 1))
167
+ return WindowsError(GetLastError());
168
+ const DWORD required =
169
+ FILE_SUPPORTS_HARD_LINKS | FILE_SUPPORTS_OPEN_BY_FILE_ID;
170
+ *supported =
171
+ IsNtfs(filesystem_name) && (filesystem_flags & required) == required;
172
+ return 0;
173
+ }
174
+
175
+ // Node and Bun own the Windows CRT descriptor table. The TypeScript adapter
176
+ // uses their fsyncSync implementation, which can safely reach the owning table.
177
+ int SyncDescriptor(int) { return ENOSYS; }
178
+
179
+ } // namespace neottia::repository_store
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neottia/repository-store",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Crash-recoverable repository-local canonical file and disposable SQLite storage primitives",
5
5
  "author": "Dragos Cirjan",
6
6
  "license": "MIT",
@@ -50,13 +50,13 @@
50
50
  "node": ">=22.16.0"
51
51
  },
52
52
  "dependencies": {
53
- "node-gyp": "^11.5.0"
53
+ "node-gyp": "^13.0.2"
54
54
  },
55
55
  "devDependencies": {
56
- "@templ-project/tsconfig": "^0.2.2",
57
- "@types/node": "^25.0.3",
56
+ "@templ-project/tsconfig": "^0.4.2",
57
+ "@types/node": "^26.6.4",
58
58
  "typescript": "^5.9.3",
59
- "vitest": "^4.1.11"
59
+ "vitest": "^5.0.3"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "pnpm run build:native && node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
@@ -1,10 +1,10 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
 
3
- if (process.platform === 'linux') {
3
+ if (process.platform === 'linux' || process.platform === 'darwin' || process.platform === 'win32') {
4
4
  const command = process.platform === 'win32' ? 'node-gyp.cmd' : 'node-gyp';
5
5
  const result = spawnSync(command, ['rebuild'], { stdio: 'inherit', shell: true });
6
6
  if (result.status !== 0)
7
7
  console.warn(
8
- '@neottia/repository-store: native exact-publication backend was not built; destructive filesystem mutations will fail with UNSUPPORTED_RUNTIME.',
8
+ '@neottia/repository-store: native platform backend was not built; destructive filesystem mutations will fail with UNSUPPORTED_RUNTIME.',
9
9
  );
10
10
  }
@@ -1,9 +0,0 @@
1
- export interface NativePublicationBackend {
2
- renameNoReplace(source: string, destination: string): void;
3
- supportsLocalPath(path: string): boolean;
4
- }
5
- export declare function requireNativePublicationBackend(): NativePublicationBackend;
6
- export declare function nativeRenameNoReplace(backend: NativePublicationBackend, source: string, destination: string): void;
7
- export declare function assertNativePublicationPath(backend: NativePublicationBackend, path: string): void;
8
- export declare function setNativePublicationBackendForTests(backend: NativePublicationBackend | null | undefined): void;
9
- //# sourceMappingURL=native-publication.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"native-publication.d.ts","sourceRoot":"","sources":["../../src/internal/native-publication.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,wBAAwB;IACvC,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3D,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1C;AAOD,wBAAgB,+BAA+B,IAAI,wBAAwB,CAuB1E;AAGD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAQlH;AAGD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,wBAAwB,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAOjG;AAGD,wBAAgB,mCAAmC,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAE9G"}
@@ -1,65 +0,0 @@
1
- import { createRequire } from 'node:module';
2
- import { fileURLToPath } from 'node:url';
3
- import { UnsupportedRuntimeError } from '../errors.js';
4
- let injectedBackend;
5
- let loadedBackend;
6
- let loadFailure;
7
- export function requireNativePublicationBackend() {
8
- if (injectedBackend !== undefined) {
9
- if (injectedBackend !== null)
10
- return injectedBackend;
11
- throw unsupported('Native publication backend is disabled for this operation.');
12
- }
13
- if (process.platform !== 'linux')
14
- throw unsupported(`Exact destructive publication is unsupported on ${process.platform}.`);
15
- if (loadedBackend !== undefined)
16
- return loadedBackend;
17
- if (loadFailure !== undefined)
18
- throw unsupported('Native publication backend could not be loaded.', loadFailure);
19
- try {
20
- const require = createRequire(import.meta.url);
21
- const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
22
- const candidate = require(`${packageRoot}build/Release/repository_store_native.node`);
23
- if (typeof candidate.renameNoReplace !== 'function' || typeof candidate.supportsLocalPath !== 'function')
24
- throw new Error('Native addon does not expose the complete publication contract.');
25
- loadedBackend = candidate;
26
- return loadedBackend;
27
- }
28
- catch (error) {
29
- loadFailure = error;
30
- throw unsupported('Native publication backend could not be loaded.', error);
31
- }
32
- }
33
- export function nativeRenameNoReplace(backend, source, destination) {
34
- try {
35
- backend.renameNoReplace(source, destination);
36
- }
37
- catch (error) {
38
- if (['EINVAL', 'EXDEV', 'ENOSYS', 'ENOTSUP', 'EOPNOTSUPP'].some((code) => hasCode(error, code)))
39
- throw unsupported(`Native no-replace move is unsupported for ${source}.`, error);
40
- throw error;
41
- }
42
- }
43
- export function assertNativePublicationPath(backend, path) {
44
- try {
45
- if (backend.supportsLocalPath(path))
46
- return;
47
- }
48
- catch (error) {
49
- throw unsupported(`Native filesystem capability check failed for ${path}.`, error);
50
- }
51
- throw unsupported(`Exact destructive publication requires a supported Linux local filesystem: ${path}.`);
52
- }
53
- export function setNativePublicationBackendForTests(backend) {
54
- injectedBackend = backend;
55
- }
56
- function hasCode(error, code) {
57
- return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
58
- }
59
- function unsupported(message, cause) {
60
- const error = new UnsupportedRuntimeError(message);
61
- if (cause !== undefined)
62
- Object.defineProperty(error, 'cause', { value: cause });
63
- return error;
64
- }
65
- //# sourceMappingURL=native-publication.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"native-publication.js","sourceRoot":"","sources":["../../src/internal/native-publication.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAQvD,IAAI,eAA4D,CAAC;AACjE,IAAI,aAAmD,CAAC;AACxD,IAAI,WAAoB,CAAC;AAGzB,MAAM,UAAU,+BAA+B;IAC7C,IAAI,eAAe,KAAK,SAAS,EAAE,CAAC;QAClC,IAAI,eAAe,KAAK,IAAI;YAAE,OAAO,eAAe,CAAC;QACrD,MAAM,WAAW,CAAC,4DAA4D,CAAC,CAAC;IAClF,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAC9B,MAAM,WAAW,CAAC,mDAAmD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IAC5F,IAAI,aAAa,KAAK,SAAS;QAAE,OAAO,aAAa,CAAC;IACtD,IAAI,WAAW,KAAK,SAAS;QAAE,MAAM,WAAW,CAAC,iDAAiD,EAAE,WAAW,CAAC,CAAC;IACjH,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/C,MAAM,WAAW,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QACtE,MAAM,SAAS,GAAG,OAAO,CACvB,GAAG,WAAW,4CAA4C,CACtB,CAAC;QACvC,IAAI,OAAO,SAAS,CAAC,eAAe,KAAK,UAAU,IAAI,OAAO,SAAS,CAAC,iBAAiB,KAAK,UAAU;YACtG,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAC;QACrF,aAAa,GAAG,SAAqC,CAAC;QACtD,OAAO,aAAa,CAAC;IACvB,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB,WAAW,GAAG,KAAK,CAAC;QACpB,MAAM,WAAW,CAAC,iDAAiD,EAAE,KAAK,CAAC,CAAC;IAC9E,CAAC;AACH,CAAC;AAGD,MAAM,UAAU,qBAAqB,CAAC,OAAiC,EAAE,MAAc,EAAE,WAAmB;IAC1G,IAAI,CAAC;QACH,OAAO,CAAC,eAAe,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC/C,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAC7F,MAAM,WAAW,CAAC,6CAA6C,MAAM,GAAG,EAAE,KAAK,CAAC,CAAC;QACnF,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAGD,MAAM,UAAU,2BAA2B,CAAC,OAAiC,EAAE,IAAY;IACzF,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC;YAAE,OAAO;IAC9C,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB,MAAM,WAAW,CAAC,iDAAiD,IAAI,GAAG,EAAE,KAAK,CAAC,CAAC;IACrF,CAAC;IACD,MAAM,WAAW,CAAC,8EAA8E,IAAI,GAAG,CAAC,CAAC;AAC3G,CAAC;AAGD,MAAM,UAAU,mCAAmC,CAAC,OAAoD;IACtG,eAAe,GAAG,OAAO,CAAC;AAC5B,CAAC;AAED,SAAS,OAAO,CAAC,KAAc,EAAE,IAAY;IAC3C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;AAC/F,CAAC;AAED,SAAS,WAAW,CAAC,OAAe,EAAE,KAAe;IACnD,MAAM,KAAK,GAAG,IAAI,uBAAuB,CAAC,OAAO,CAAC,CAAC;IACnD,IAAI,KAAK,KAAK,SAAS;QAAE,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IACjF,OAAO,KAAK,CAAC;AACf,CAAC"}