sliftutils 1.2.26 → 1.2.38
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/CLAUDE.md +211 -0
- package/index.d.ts +11 -0
- package/package.json +1 -1
- package/storage/FileFolderAPI.d.ts +3 -0
- package/storage/FileFolderAPI.tsx +32 -2
- package/storage/IStorage.d.ts +4 -0
- package/storage/IStorage.ts +3 -0
- package/storage/IndexedDBFileFolderAPI.ts +6 -0
- package/storage/PrivateFileSystemStorage.d.ts +4 -0
- package/storage/PrivateFileSystemStorage.ts +11 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Project rules
|
|
2
|
+
|
|
3
|
+
These are the binding rules for working in this repo. They were imported
|
|
4
|
+
from `.cursor/rules/*.mdc` so Claude reads them automatically.
|
|
5
|
+
|
|
6
|
+
## General guidelines
|
|
7
|
+
|
|
8
|
+
- The code automatically updates on save, so do not ever run commands to rerun the site.
|
|
9
|
+
- Don't run shell commands when you need to create or move small code files. Use tool calls. Use tool calls to make files within folders — you don't need to make the folder, just make the file, the folder will be created automatically.
|
|
10
|
+
- If you need to add a dependency, don't just edit `package.json`. Use `yarn add` so you get the latest version, unless the user specifies a version.
|
|
11
|
+
- Use tool calls to read files and directories instead of running `ls`, `dir`, etc.
|
|
12
|
+
|
|
13
|
+
## Coding styles
|
|
14
|
+
|
|
15
|
+
- Times should almost always be in milliseconds; assume milliseconds if not told otherwise.
|
|
16
|
+
- Don't make functions that will never be reused and are short. If under 5 lines and not reused, don't create it unless explicitly told to.
|
|
17
|
+
- Comments are used sparingly and only when required to explain what's being done. A comment that just restates the function name is forbidden.
|
|
18
|
+
- Comments go on the line BEFORE the statement, never trailing the semicolon.
|
|
19
|
+
- Use `undefined`, not `null`.
|
|
20
|
+
- Almost never check for `undefined`/`null` specifically — just check truthiness.
|
|
21
|
+
- When a function has more than one primitive parameter that could be confused (e.g. start and end time), put them inside a single object parameter called `config`.
|
|
22
|
+
- Never use return codes — always throw. Include context (expected vs actual). If values could be huge (e.g. file parsing), limit to ~500 characters.
|
|
23
|
+
- Use double quotes, not single quotes.
|
|
24
|
+
- Never use the ternary operator. Convert `x ? y : z` into `x && y || z`.
|
|
25
|
+
- Never use the non-null assertion operator (`!`). Check the value; if needed in nested closures, copy into a `const` to preserve narrowed type.
|
|
26
|
+
- Errors use template strings that include the actual offending value and the expected one: `throw new Error(\`Expected X, was \${y}\`);`
|
|
27
|
+
- Don't use `switch`. Use `if/else`.
|
|
28
|
+
- Don't use `!` to access a value from a `Map`. Use `get` + initialize-if-undefined + `set`.
|
|
29
|
+
- Sort with `import { sort } from "socket-function/src/misc";` — `sort<T>(arr: T[], sortKey: (obj: T) => unknown)`.
|
|
30
|
+
- Prefer early `return` over deep `else`. Handle error cases, warn/throw, then return. The main case should be at the bottom, not nested.
|
|
31
|
+
- Use functions to remove duplication only when something is actually duplicated.
|
|
32
|
+
- Don't recreate collections or URL parameters — import them.
|
|
33
|
+
- Do not redefine types. Import them.
|
|
34
|
+
- Do not annotate types that can be inferred.
|
|
35
|
+
- Constants that might need reconfiguration go near the top of the file under the imports, not buried in functions.
|
|
36
|
+
- Never use environment variables. Configuration goes on disk or via CLI args.
|
|
37
|
+
- Never use inline styles. Always use the `css` helper.
|
|
38
|
+
- Don't use `as any`.
|
|
39
|
+
- When fetch returns `any`, cast it to the real type rather than leaving it as `any`. Same for any deserialized value.
|
|
40
|
+
- DO NOT redeclare constants or types — IMPORT THEM.
|
|
41
|
+
- Don't try/catch for no reason. If you can't handle the exception, let it throw.
|
|
42
|
+
- For input events, always use `event.currentTarget`.
|
|
43
|
+
- Use `ref={elem => …}` callbacks. NEVER use `React.createRef`.
|
|
44
|
+
- NEVER render images with a fixed width AND height. This stretches or crops them. Set only width OR height.
|
|
45
|
+
- Avoid callback hell with `import { PromiseObj } from "socket-function/src/misc";` — wrap event callbacks in a `PromiseObj` and await it.
|
|
46
|
+
- `import { keyBy, keyByArray } from "socket-function/src/misc";` for building lookups.
|
|
47
|
+
- Never use `alert`. Throw instead.
|
|
48
|
+
|
|
49
|
+
## MobX state
|
|
50
|
+
|
|
51
|
+
We use MobX. Components store local state in a field called `synced`, which is an `observable`. Never use `Component.state`. Components need the `@observer` decorator.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import preact from "preact";
|
|
55
|
+
import { observable } from "mobx";
|
|
56
|
+
import { observer } from "sliftutils/render-utils/observer";
|
|
57
|
+
|
|
58
|
+
@observer
|
|
59
|
+
class Example extends preact.Component {
|
|
60
|
+
synced = observable({
|
|
61
|
+
x: 0,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
render() {
|
|
65
|
+
return <div>
|
|
66
|
+
<button onClick={() => this.synced.x++}>
|
|
67
|
+
Click me
|
|
68
|
+
</button>
|
|
69
|
+
<p>
|
|
70
|
+
{this.synced.x}
|
|
71
|
+
</p>
|
|
72
|
+
</div>;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Styling and CSS
|
|
78
|
+
|
|
79
|
+
- Never use `em` or `rem`. Use `px` or `vw`/`vh`/`%`.
|
|
80
|
+
- Don't add font colors / aesthetics / `fontSize` beyond `hbox`/`vbox`/`pad2` unless asked. If you think styling could help, *tell the user* — don't add it unprompted.
|
|
81
|
+
- Never use `h1`/`h2`/`h3` etc. — set the font size explicitly instead.
|
|
82
|
+
- Don't use `fillWidth` where `flexGrow(1)` would do.
|
|
83
|
+
- Add very little styling (colors, rounding, etc.) unless asked.
|
|
84
|
+
|
|
85
|
+
### The `css` helper
|
|
86
|
+
|
|
87
|
+
All styling goes through the `css` helper.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
<div className={css.width(100).height(50)}>…</div>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Chains of properties are fine across two lines:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
className={css.size(100, 100).hbox(4)
|
|
97
|
+
.hsl(0, 50, 50).borderRadius(4)
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Conditionals come after, never as a ternary:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
className={css
|
|
105
|
+
.size(100, 100).hbox(4)
|
|
106
|
+
+ (isDimmed && css.opacity(0.5))
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Aliases
|
|
111
|
+
|
|
112
|
+
Non-call aliases (chainable): `relative`, `absolute`, `fixed`, `wrap`, `marginAuto`, `fillBoth`, `fillWidth`, `fillHeight`, `flexShrink0`, `ellipsis`, `overflowAuto`, `overflowHidden`.
|
|
113
|
+
|
|
114
|
+
Call aliases: `hbox(gap, rowGap?)`, `vbox(gap, columnGap?)`, `pad2(value, vertical?)`, `hsl/hsla(...)`, `hslhover/hslahover`, `bord/bord2`, `hslcolor/hslacolor`, `size(w, h)`, `pos(x, y)`.
|
|
115
|
+
|
|
116
|
+
Use `css.button` to make a *non-button* feel like a button (hover background + pointer cursor) — only when a background color is set, and never on actual `<button>`/`<Button>`.
|
|
117
|
+
|
|
118
|
+
Prefer `hbox`/`vbox` for spacing between elements over margins.
|
|
119
|
+
|
|
120
|
+
### Animations
|
|
121
|
+
|
|
122
|
+
Keyframes go in a `<style>` tag:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<style>{`
|
|
126
|
+
@keyframes spinner-ring {
|
|
127
|
+
0% { transform: rotate(0deg); }
|
|
128
|
+
100% { transform: rotate(360deg); }
|
|
129
|
+
}
|
|
130
|
+
`}</style>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Components
|
|
134
|
+
|
|
135
|
+
### Anchor
|
|
136
|
+
|
|
137
|
+
`Anchor` is the `<a>` for navigation tied to `URLParam`:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<Anchor params={[[todolistURL, listKey]]}>
|
|
141
|
+
{list.name}
|
|
142
|
+
</Anchor>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`URLParam` stores a value in the URL. Second argument is the default (number, string, or object). Use `.value` to get/set.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const todolistURL = new URLParam("todolist", "");
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### InputLabel
|
|
152
|
+
|
|
153
|
+
Use `InputLabel` / `InputLabelURL` for inputs:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
<InputLabelURL
|
|
157
|
+
label="Show Previous Video"
|
|
158
|
+
checkbox
|
|
159
|
+
persisted={showPreviousVideoURL}
|
|
160
|
+
/>
|
|
161
|
+
<InputLabel
|
|
162
|
+
label="Notes"
|
|
163
|
+
fillWidth
|
|
164
|
+
value={node.notes || ""}
|
|
165
|
+
onChangeValue={async (value) => {
|
|
166
|
+
const updatedNode = deepCloneJSON(node);
|
|
167
|
+
updatedNode.notes = value;
|
|
168
|
+
await VideoNode.set(node.id, updatedNode);
|
|
169
|
+
}}
|
|
170
|
+
/>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## DiskCollection
|
|
174
|
+
|
|
175
|
+
If you read from a collection, mutate, and want to write back, shallow-copy so the collection notices the change:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
let x = collection.get("x");
|
|
179
|
+
x.y = Math.random();
|
|
180
|
+
collection.set("x", { ...x });
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Non-async getters go in render functions; async getters go in event handlers. `.set` works in both.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
get(key: string): T | undefined;
|
|
187
|
+
async getPromise(key: string): Promise<T | undefined>;
|
|
188
|
+
set(key: string, value: T): void;
|
|
189
|
+
remove(key: string): void;
|
|
190
|
+
getKeys(): string[];
|
|
191
|
+
getKeysPromise(): Promise<string[]>;
|
|
192
|
+
getEntries(): [string, T][];
|
|
193
|
+
getValues(): T[];
|
|
194
|
+
async getValuesPromise(): Promise<T[]>;
|
|
195
|
+
getInfo(key: string);
|
|
196
|
+
async reset();
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## API calls
|
|
200
|
+
|
|
201
|
+
In an event callback (which must be async):
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
APIController(getExtNodeId()).getModels.promise()
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
In a render function:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
APIController(getExtNodeId()).getModels()
|
|
211
|
+
```
|
package/index.d.ts
CHANGED
|
@@ -929,6 +929,9 @@ declare module "sliftutils/storage/FileFolderAPI" {
|
|
|
929
929
|
size: number;
|
|
930
930
|
lastModified: number;
|
|
931
931
|
arrayBuffer(): Promise<ArrayBuffer>;
|
|
932
|
+
slice(start: number, end: number): {
|
|
933
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
934
|
+
};
|
|
932
935
|
}>;
|
|
933
936
|
createWritable(config?: {
|
|
934
937
|
keepExistingData?: boolean;
|
|
@@ -1026,6 +1029,10 @@ declare module "sliftutils/storage/IStorage" {
|
|
|
1026
1029
|
};
|
|
1027
1030
|
export type IStorageRaw = {
|
|
1028
1031
|
get(key: string): Promise<Buffer | undefined>;
|
|
1032
|
+
getRange(key: string, config: {
|
|
1033
|
+
start: number;
|
|
1034
|
+
end: number;
|
|
1035
|
+
}): Promise<Buffer | undefined>;
|
|
1029
1036
|
append(key: string, value: Buffer): Promise<void>;
|
|
1030
1037
|
set(key: string, value: Buffer): Promise<void>;
|
|
1031
1038
|
remove(key: string): Promise<void>;
|
|
@@ -1117,6 +1124,10 @@ declare module "sliftutils/storage/PrivateFileSystemStorage" {
|
|
|
1117
1124
|
private getFileHandle;
|
|
1118
1125
|
private fileExists;
|
|
1119
1126
|
get(key: string): Promise<Buffer | undefined>;
|
|
1127
|
+
getRange(key: string, config: {
|
|
1128
|
+
start: number;
|
|
1129
|
+
end: number;
|
|
1130
|
+
}): Promise<Buffer | undefined>;
|
|
1120
1131
|
set(key: string, value: Buffer): Promise<void>;
|
|
1121
1132
|
append(key: string, value: Buffer): Promise<void>;
|
|
1122
1133
|
remove(key: string): Promise<void>;
|
package/package.json
CHANGED
|
@@ -40,6 +40,9 @@ type FileWrapper = {
|
|
|
40
40
|
size: number;
|
|
41
41
|
lastModified: number;
|
|
42
42
|
arrayBuffer(): Promise<ArrayBuffer>;
|
|
43
|
+
// Matches Blob.slice (which the native File object provides), so the browser
|
|
44
|
+
// implementation works vanilla. End is exclusive, both clamped to the file size.
|
|
45
|
+
slice(start: number, end: number): { arrayBuffer(): Promise<ArrayBuffer> };
|
|
43
46
|
}>;
|
|
44
47
|
createWritable(config?: { keepExistingData?: boolean }): Promise<{
|
|
45
48
|
seek(offset: number): Promise<void>;
|
|
@@ -99,13 +102,29 @@ class NodeJSFileHandleWrapper implements FileWrapper {
|
|
|
99
102
|
|
|
100
103
|
async getFile() {
|
|
101
104
|
const stats = await fs.promises.stat(this.filePath);
|
|
105
|
+
const filePath = this.filePath;
|
|
102
106
|
return {
|
|
103
107
|
size: stats.size,
|
|
104
108
|
lastModified: stats.mtimeMs,
|
|
105
109
|
arrayBuffer: async () => {
|
|
106
|
-
const buffer = await fs.promises.readFile(
|
|
110
|
+
const buffer = await fs.promises.readFile(filePath);
|
|
107
111
|
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
108
|
-
}
|
|
112
|
+
},
|
|
113
|
+
slice: (start: number, end: number) => ({
|
|
114
|
+
arrayBuffer: async () => {
|
|
115
|
+
const clampedStart = Math.min(Math.max(start, 0), stats.size);
|
|
116
|
+
const clampedEnd = Math.min(Math.max(end, clampedStart), stats.size);
|
|
117
|
+
const length = clampedEnd - clampedStart;
|
|
118
|
+
const fileHandle = await fs.promises.open(filePath, "r");
|
|
119
|
+
try {
|
|
120
|
+
const buffer = Buffer.alloc(length);
|
|
121
|
+
await fileHandle.read(buffer, 0, length, clampedStart);
|
|
122
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
123
|
+
} finally {
|
|
124
|
+
await fileHandle.close();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
})
|
|
109
128
|
};
|
|
110
129
|
}
|
|
111
130
|
|
|
@@ -440,6 +459,17 @@ function wrapHandleFiles(handle: DirectoryWrapper): IStorageRaw {
|
|
|
440
459
|
}
|
|
441
460
|
},
|
|
442
461
|
|
|
462
|
+
async getRange(key: string, config: { start: number; end: number }): Promise<Buffer | undefined> {
|
|
463
|
+
try {
|
|
464
|
+
const file = await handle.getFileHandle(key);
|
|
465
|
+
const fileContent = await file.getFile();
|
|
466
|
+
const arrayBuffer = await fileContent.slice(config.start, config.end).arrayBuffer();
|
|
467
|
+
return Buffer.from(arrayBuffer);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
return undefined;
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
|
|
443
473
|
async append(key: string, value: Buffer): Promise<void> {
|
|
444
474
|
await appendQueue(key)(async () => {
|
|
445
475
|
// NOTE: Interesting point. Chrome doesn't optimize this to be an append, and instead
|
package/storage/IStorage.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ export type IStorage<T> = {
|
|
|
27
27
|
};
|
|
28
28
|
export type IStorageRaw = {
|
|
29
29
|
get(key: string): Promise<Buffer | undefined>;
|
|
30
|
+
getRange(key: string, config: {
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
}): Promise<Buffer | undefined>;
|
|
30
34
|
append(key: string, value: Buffer): Promise<void>;
|
|
31
35
|
set(key: string, value: Buffer): Promise<void>;
|
|
32
36
|
remove(key: string): Promise<void>;
|
package/storage/IStorage.ts
CHANGED
|
@@ -29,6 +29,9 @@ export type IStorage<T> = {
|
|
|
29
29
|
// (/ makes a folder). And there are even more rules, such as lengths per folder, etc, etc.
|
|
30
30
|
export type IStorageRaw = {
|
|
31
31
|
get(key: string): Promise<Buffer | undefined>;
|
|
32
|
+
// Reads bytes in the range [start, end) (end is exclusive, clamped to the file size).
|
|
33
|
+
// Returns undefined if the file doesn't exist.
|
|
34
|
+
getRange(key: string, config: { start: number; end: number }): Promise<Buffer | undefined>;
|
|
32
35
|
// May or may not be efficient in the underlying storage
|
|
33
36
|
append(key: string, value: Buffer): Promise<void>;
|
|
34
37
|
set(key: string, value: Buffer): Promise<void>;
|
|
@@ -44,6 +44,12 @@ class VirtualFileStorage implements FileStorage {
|
|
|
44
44
|
return badBuffer;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
async getRange(key: string, config: { start: number; end: number }): Promise<Buffer | undefined> {
|
|
48
|
+
const fullBuffer = await this.get(key);
|
|
49
|
+
if (!fullBuffer) return undefined;
|
|
50
|
+
return fullBuffer.subarray(config.start, config.end);
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
async append(key: string, value: Buffer): Promise<void> {
|
|
48
54
|
const store = this.getStore("readwrite");
|
|
49
55
|
const fullPath = this.id + key;
|
|
@@ -11,6 +11,10 @@ export declare class PrivateFileSystemStorage implements IStorageRaw {
|
|
|
11
11
|
private getFileHandle;
|
|
12
12
|
private fileExists;
|
|
13
13
|
get(key: string): Promise<Buffer | undefined>;
|
|
14
|
+
getRange(key: string, config: {
|
|
15
|
+
start: number;
|
|
16
|
+
end: number;
|
|
17
|
+
}): Promise<Buffer | undefined>;
|
|
14
18
|
set(key: string, value: Buffer): Promise<void>;
|
|
15
19
|
append(key: string, value: Buffer): Promise<void>;
|
|
16
20
|
remove(key: string): Promise<void>;
|
|
@@ -114,6 +114,17 @@ export class PrivateFileSystemStorage implements IStorageRaw {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
public async getRange(key: string, config: { start: number; end: number }): Promise<Buffer | undefined> {
|
|
118
|
+
const fileHandle = await this.getFileHandle(key, false);
|
|
119
|
+
if (!fileHandle) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const file = await fileHandle.getFile();
|
|
124
|
+
const arrayBuffer = await file.slice(config.start, config.end).arrayBuffer();
|
|
125
|
+
return Buffer.from(arrayBuffer);
|
|
126
|
+
}
|
|
127
|
+
|
|
117
128
|
public async set(key: string, value: Buffer): Promise<void> {
|
|
118
129
|
try {
|
|
119
130
|
const fileHandle = await this.getFileHandle(key, true);
|