@avantf/dsh-plugin-base 0.5.3 → 0.6.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 +132 -13
- package/lib/bootstrap.d.ts +1 -1
- package/lib/bootstrap.js +1 -1
- package/lib/client/load_more.d.ts +157 -0
- package/lib/client/load_more.d.ts.map +1 -0
- package/lib/client/load_more.js +252 -0
- package/lib/conformance.d.ts.map +1 -1
- package/lib/conformance.js +5 -0
- package/lib/errors.d.ts +3 -1
- package/lib/errors.d.ts.map +1 -1
- package/lib/fs.d.ts.map +1 -1
- package/lib/fs.js +3 -0
- package/lib/index.d.ts +10 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +18 -3
- package/lib/interface.d.ts +165 -32
- package/lib/interface.d.ts.map +1 -1
- package/lib/interface.js +56 -16
- package/lib/internal.d.ts +4 -0
- package/lib/internal.d.ts.map +1 -1
- package/lib/internal.js +22 -0
- package/lib/lint.d.ts.map +1 -1
- package/lib/lint.js +39 -1
- package/lib/providers/npm.d.ts +10 -1
- package/lib/providers/npm.d.ts.map +1 -1
- package/lib/providers/npm.js +2 -2
- package/lib/providers/runtime_deps.d.ts +184 -0
- package/lib/providers/runtime_deps.d.ts.map +1 -0
- package/lib/providers/runtime_deps.js +1214 -0
- package/lib/provisioner.d.ts.map +1 -1
- package/lib/provisioner.js +4 -1
- package/lib/runtime_deps_helper.d.ts +71 -0
- package/lib/runtime_deps_helper.d.ts.map +1 -0
- package/lib/runtime_deps_helper.js +161 -0
- package/lib/session_notices.d.ts +95 -0
- package/lib/session_notices.d.ts.map +1 -0
- package/lib/session_notices.js +172 -0
- package/lib/state.d.ts +8 -1
- package/lib/state.d.ts.map +1 -1
- package/lib/state.js +20 -4
- package/lib/types.d.ts +14 -1
- package/lib/types.d.ts.map +1 -1
- package/package.json +7 -4
- package/src/client/controls.module.css +294 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @avantf/dsh-plugin-base
|
|
2
2
|
|
|
3
3
|
DSH 插件共用的底座(共享库)。**插件在运行时按 file URL 动态加载它**——不静态 `import`、不打包进插件,
|
|
4
|
-
|
|
5
|
-
无关的共享工具**。
|
|
4
|
+
拿不到就降级挂载——把四件容易各写一遍的事收在一起:**启动期资源预装**、**共享运行期依赖**、
|
|
5
|
+
**宿主兼容门禁**、**一组与 DSH 无关的共享工具**。
|
|
6
6
|
|
|
7
7
|
- **运行期零依赖**:`dependencies` 为空,源码里连 `zod` 都不 import。
|
|
8
8
|
- 唯一一条 peer 是 `zod`(`>=4.4.3 <5`),由宿主 / 上层提供,好让调用方、本包与宿主解析到**同一份**
|
|
@@ -25,11 +25,38 @@ DSH 插件共用的底座(共享库)。**插件在运行时按 file URL 动
|
|
|
25
25
|
`warn`(写明 holder 的 pid、已持有时长、本次已等待时长)后继续按预算等待;等不到就报 `lock/timeout`,
|
|
26
26
|
消息里带 holder 与 mtime 诊断;
|
|
27
27
|
- 每一项给出 `present / installed / skipped / failed`,失败带稳定 `code`;
|
|
28
|
-
-
|
|
29
|
-
|
|
28
|
+
- 四种内置资源种类:`npm-package`、`binary-archive`、`model-cache`、`runtime-deps`。新的种类 =
|
|
29
|
+
一个新的 provider,核心不动;
|
|
30
30
|
- 策略可配:离线、镜像、每项的并发与超时、启动预算、下载总闸。
|
|
31
31
|
|
|
32
|
-
### 2.
|
|
32
|
+
### 2. 共享运行期依赖(runtime-deps)
|
|
33
|
+
|
|
34
|
+
插件需要**自己 install 树里没有**的运行期 npm 包(例如某个带原生模块的推理库),但两个插件不该各装一份、
|
|
35
|
+
更不该把宿主单例(`zod`、DSH 包)装成两份。base 提供一个**按 spec 寻址(spec-addressed)、装一次就不再
|
|
36
|
+
改写、全局共享**的依赖根:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
插件声明(packages / sources / attempts / verify / peers)
|
|
40
|
+
→ key = sha256(packages + verify + lockfile + platform/arch/Node ABI + manager 版本
|
|
41
|
+
+ attempts 的 source/env/scripts + peer 名/区间 + 解析到的宿主副本)
|
|
42
|
+
→ 在 <home>/deps/runtime/<key>/ 只装一次(per-key 锁 + staging + 原子 rename)
|
|
43
|
+
→ 插件用 createRequire(<根>/package.json) 加载
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- key 是**声明**的摘要,不是取回字节的摘要:不给 `lockfile` 时,同一个 key 在不同时间/机器上可以装出不同
|
|
47
|
+
版本;根保证的是“装一次,之后本地不再改写”。要字节级可复现就把 `lockfile` 传进来;
|
|
48
|
+
- 同 key ⇒ 同一份物理安装,所有插件共用;改了包、verify、lockfile、平台、manager、env,或 peer 的区间/
|
|
49
|
+
解析到的副本 ⇒ 新 key、新根,**旧根永不被改写**;
|
|
50
|
+
- `sources` + `attempts` 按顺序尝试(registry / cache / 离线 / 脚本策略);每个 attempt 都记录,全部失败报
|
|
51
|
+
`runtime-deps/install-failed`,插件照常挂载(`failed` 只是一条报告);
|
|
52
|
+
- `verify` 的每一项在写 `.ready` 之前用 `createRequire(...).resolve()` 验过;失败换下一个 attempt;
|
|
53
|
+
- `peers` **只链接不安装**:宿主单例写在 `peerDependencies`(optional),装完从插件自己的树链接进来;复用
|
|
54
|
+
时同时核对**链接目标**与链接到的版本,同版本的另一个目录不算本 key 的根(没有 symlink 时的复制回退是
|
|
55
|
+
版本级身份,记录在 `.runtime.json` 的 `copiedPeers`);
|
|
56
|
+
- **base 不认识任何包名**:装什么、从哪装、验什么,全部来自插件的 `RuntimeDepsSpec`;
|
|
57
|
+
- GC **显式**(`./internal` 的 `gcRuntimeDeps`),按 `status.json` 里记录的 key 保留,绝不在启动时自动回收。
|
|
58
|
+
|
|
59
|
+
### 3. 宿主兼容门禁
|
|
33
60
|
|
|
34
61
|
在挂载之前判断"当前宿主能不能跑这段代码",语义是**只有被证实的不兼容才拒绝**:
|
|
35
62
|
|
|
@@ -39,7 +66,7 @@ DSH 插件共用的底座(共享库)。**插件在运行时按 file URL 动
|
|
|
39
66
|
- `registerMegaphone` 在拒绝加载的路径上留一条用户可见的命令(那时它是唯一的信息渠道);
|
|
40
67
|
- `verifyRegisteredFaces` 在挂载后复查"声明的每个 schema / 工具真的注册上了"(只告警)。
|
|
41
68
|
|
|
42
|
-
###
|
|
69
|
+
### 4. 共享工具(kit)
|
|
43
70
|
|
|
44
71
|
与 DSH 无关的纯逻辑,调用方在运行时复用:
|
|
45
72
|
|
|
@@ -259,6 +286,87 @@ if (!verdict.load) {
|
|
|
259
286
|
}
|
|
260
287
|
```
|
|
261
288
|
|
|
289
|
+
### 取用共享运行期依赖(runtime-deps)
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
// 注册 provider(它和别的 provider 一样进 provisioner)
|
|
293
|
+
provisioner.register(framework.runtimeDepsProvider())
|
|
294
|
+
|
|
295
|
+
provisioner.declare({
|
|
296
|
+
plugin: 'my-plugin',
|
|
297
|
+
items: [{
|
|
298
|
+
id: 'my-plugin:transformers',
|
|
299
|
+
kind: framework.RUNTIME_DEPS_KIND, // 'runtime-deps'
|
|
300
|
+
spec: {
|
|
301
|
+
packages: { '@huggingface/transformers': '^4.2.0' },
|
|
302
|
+
sources: [
|
|
303
|
+
{ id: 'npmmirror', registry: 'https://registry.npmmirror.com' },
|
|
304
|
+
{ id: 'npmjs', registry: 'https://registry.npmjs.org' },
|
|
305
|
+
],
|
|
306
|
+
attempts: [
|
|
307
|
+
{ source: 'npmmirror' },
|
|
308
|
+
{ source: 'npmmirror', env: { ONNXRUNTIME_NODE_INSTALL: 'skip' } },
|
|
309
|
+
{ source: 'npmjs', manager: 'npm' },
|
|
310
|
+
],
|
|
311
|
+
verify: ['@huggingface/transformers'],
|
|
312
|
+
peers: { zod: '>=4.4.3 <5' }, // 宿主单例:只链接,不从 registry 装
|
|
313
|
+
},
|
|
314
|
+
target: { root: 'deps/runtime' }, // 规范位置;换 root 就是另一个共享根
|
|
315
|
+
onMissing: { atStartup: 'degrade', atUse: 'error' },
|
|
316
|
+
schemaVersion: 1,
|
|
317
|
+
}],
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const report = await provisioner.ensure({ only: ['my-plugin:transformers'], deadlineMs: 120_000 })
|
|
321
|
+
if (!report.ok) logger.warn('运行期依赖未就绪,用内置降级路径继续')
|
|
322
|
+
|
|
323
|
+
const state = provisioner.resolve('my-plugin:transformers')
|
|
324
|
+
if (state.state === 'ready') {
|
|
325
|
+
const dir = state.handle.dir // <home>/deps/runtime/<key>
|
|
326
|
+
const req = createRequire(join(dir, 'package.json'))
|
|
327
|
+
const transformers = await import(req.resolve('@huggingface/transformers'))
|
|
328
|
+
// 完整记录(key / attempts / integrity / installedAt)在 <dir>/.runtime.json
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### 用 helper 声明运行期依赖 + 会话提示(v6)
|
|
333
|
+
|
|
334
|
+
上面那种接线(注册 provider、拼 item、`ensure`、`createRequire`)可以缩成一次调用。helper 只是糖,底层
|
|
335
|
+
`runtimeDepsProvider()` + `createProvisioner()` 的行为一字未改:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
import { createRuntimeDeps } from '@avantf/dsh-plugin-base'
|
|
339
|
+
|
|
340
|
+
const runtime = createRuntimeDeps({
|
|
341
|
+
namespace: 'my-plugin', // item id 默认 my-plugin:runtime-deps
|
|
342
|
+
packages: { 'some-runtime-pkg': '^1.2.0' },
|
|
343
|
+
sources: [{ id: 'npmmirror', registry: 'https://registry.npmmirror.com' }],
|
|
344
|
+
attempts: [{ source: 'npmmirror' }],
|
|
345
|
+
verify: ['some-runtime-pkg'],
|
|
346
|
+
// 文案永远是插件的(base 里没有任何包名/命令/句子);when 决定它对应哪种状态
|
|
347
|
+
notices: [{
|
|
348
|
+
id: 'my-plugin:runtime-pkg',
|
|
349
|
+
severity: 'degraded',
|
|
350
|
+
when: 'failed',
|
|
351
|
+
body: '[my-plugin] 可选能力未就绪,已降级为内置路径。',
|
|
352
|
+
}],
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
provisioner.declare({ plugin: 'my-plugin', items: [runtime.item] })
|
|
356
|
+
const entry = await runtime.ensure(provisioner) // 这个 item 的报告条目
|
|
357
|
+
if (entry?.action === 'failed') logger.warn(entry.reason ?? '')
|
|
358
|
+
|
|
359
|
+
// 自己的 pre-step 里(owner 会话):
|
|
360
|
+
const lines = runtime.notices.consume(sessionId, { ownerOnly: true })
|
|
361
|
+
if (lines.length > 0) return [...lines, existingHint].join('\n') // 与已有提示合并成 ONE 条
|
|
362
|
+
|
|
363
|
+
// 就绪后从共享根加载:不复制包、不写 node_modules
|
|
364
|
+
const mod = await runtime.load('some-runtime-pkg')
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
`notices` 的状态是**每次 consume 重新判断**的(依赖修好了就不再提),一个 id 对每个 session 最多出一次,
|
|
368
|
+
多条同时成立时按 `degraded` > `warning` > `info` 排序合并;`consume` 永不抛。
|
|
369
|
+
|
|
262
370
|
### 取用共享工具
|
|
263
371
|
|
|
264
372
|
```ts
|
|
@@ -333,6 +441,13 @@ assertProviderConformance(report)
|
|
|
333
441
|
|
|
334
442
|
## 接口要点
|
|
335
443
|
|
|
444
|
+
- **v6(0.7.0)新增**:会话提示 registry `createSessionNotices`(+ `SessionNoticeSpec` /
|
|
445
|
+
`SessionNoticeRegistry`)与 runtime-deps 的薄 helper `createRuntimeDeps`(+ `RuntimeDepsConfig` /
|
|
446
|
+
`RuntimeDepsHandle`);v6 是 v5 的增量世代(`BaseRuntimeV6 extends BaseRuntimeV5`),没有拿走任何旧名字。
|
|
447
|
+
- **v5(0.6.0)新增**:`RUNTIME_DEPS_KIND` / `runtimeDepsProvider` 与 `DependencySource` /
|
|
448
|
+
`InstallAttempt` / `RuntimeDepsSpec`;v5 是 v4 的增量世代(`BaseRuntimeV5 extends BaseRuntimeV4`)。
|
|
449
|
+
`BuiltinKind` 增加 `'runtime-deps'`、`ProvisionCode` 增加 `runtime-deps/install-failed`(都是并集加宽)。
|
|
450
|
+
|
|
336
451
|
`.` 上的导出(值 + 类型)构成本包的**接口**,由世代号冻结;`./preset` / `./conformance` / `./bootstrap`
|
|
337
452
|
三个子路径也在承诺内,`./internal` 从不承诺稳定。
|
|
338
453
|
|
|
@@ -353,8 +468,10 @@ assertProviderConformance(report)
|
|
|
353
468
|
|
|
354
469
|
## 边界(不做的事)
|
|
355
470
|
|
|
356
|
-
-
|
|
357
|
-
|
|
471
|
+
- **不自己当包管理器**:不推导传递依赖、不做版本求解;`runtime-deps` 把安装交给调用方声明的 npm / pnpm,
|
|
472
|
+
依赖图与 lockfile 由 manager 负责,base 只做 spec 寻址、锁、校验与失败报告。
|
|
473
|
+
- **`npm-package` provider 不接管原生模块**:`binding.gyp` / `*.node` / 依赖 `postinstall` 的包会被识别并
|
|
474
|
+
拒绝;需要原生模块时走 `runtime-deps`(由 manager 安装)或 `binary-archive`。
|
|
358
475
|
- **不跳过校验**:npm 包核对 `dist.integrity`,归档核对 `sha256`。
|
|
359
476
|
- **不做进程内预热**:让文件在盘上是框架的事,读进内存是调用方自己的事。
|
|
360
477
|
|
|
@@ -362,11 +479,13 @@ assertProviderConformance(report)
|
|
|
362
479
|
|
|
363
480
|
- 接口面是 `.` 上的导出(值 + 类型)加上 `./preset` / `./conformance` / `./bootstrap` 三个子路径;`./internal`
|
|
364
481
|
是内部件,从不承诺稳定。
|
|
365
|
-
- **当前这一代(
|
|
366
|
-
`./internal
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
482
|
+
- **当前这一代(v6)是 v5 的增量**:`.` 仍然只列"插件真取用、或插件作者被期望调用"的成员,其余进
|
|
483
|
+
`./internal`。v6 新增会话提示 registry 与 runtime-deps helper(2 个值名 + 4 个类型名),没有拿走 v5 的
|
|
484
|
+
任何名字;`BaseRuntimeV6 extends BaseRuntimeV5`,所以 v1–v5 构建遇到 v6 base 仍判 `ok` + `warning`。
|
|
485
|
+
v5 本身是 v4 的增量(共享运行期依赖 provider:2 个值名 + 3 个类型名,外加 `BuiltinKind` /
|
|
486
|
+
`ProvisionCode` 两个并集加宽);v4 则是一个名字都没动的换代 —— 它加宽的是**既有成员接受的入参**:
|
|
487
|
+
`verifyRegisteredFaces({ …, scope? })` 多了个可选、不透明的 `scope`,原样透传给
|
|
488
|
+
`tools.get(name, scope)`,不传时行为与 v3 逐字相同。
|
|
370
489
|
- **v3 是第一次破坏性换代**:v2 的成员如果有消费者,v3 一个不少;但 v3 不再对 v2 做结构超集。旧调用方
|
|
371
490
|
遇到更新的 base 仍判 `ok` + `warning`,遇到更旧的 base 才降级。
|
|
372
491
|
- **接口与包版本是两条轴**:接口换代只升 `INTERFACE_VERSION` 并新增快照;包版本是普通 semver,只表达
|
package/lib/bootstrap.d.ts
CHANGED
package/lib/bootstrap.js
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, promises as fsp, readFileSync } from 'node:fs';
|
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
8
|
/** The bootstrap version; the resolved framework must satisfy {@link supportedRange}. */
|
|
9
|
-
export const VERSION = '0.
|
|
9
|
+
export const VERSION = '0.6.1';
|
|
10
10
|
/**
|
|
11
11
|
* The framework interval this bootstrap can launch.
|
|
12
12
|
*
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Scroll to the end → load the next page", as one DOM controller.
|
|
3
|
+
*
|
|
4
|
+
* **INTERNAL — no stability promise** (内部控制件,不承诺稳定): this module is reachable only through
|
|
5
|
+
* the `./internal` subpath, which the interface generation never covers (`docs/INTERFACE.md` §2/§5).
|
|
6
|
+
* It is not on `.`, it is not frozen with a generation, and a rename or a changed option shape is a
|
|
7
|
+
* base release, not a generation. Consumers (mem's 记忆 tab, mission's paged lists) follow the base.
|
|
8
|
+
*
|
|
9
|
+
* Why a DOM controller and not a React hook: this base has NO `react` peer (`package.json`), and it
|
|
10
|
+
* must stay that way — a Node user of the base must not be pulled into React, and the base ships no
|
|
11
|
+
* browser artifact. So the listener, the rAF throttle and the scrollport lookup live here, and each
|
|
12
|
+
* plugin keeps a thin wrapper of its own (mem: `useLoadMoreOnScroll`). The DOM detail then exists
|
|
13
|
+
* once instead of once per panel.
|
|
14
|
+
*
|
|
15
|
+
* Why the DOM types below are STRUCTURAL instead of `lib.dom`: the base compiles with `lib: es2023`
|
|
16
|
+
* and `types: node`, and adding `DOM` there retypes globals for the whole package (it breaks `fetch`
|
|
17
|
+
* bodies across the existing suite, for one). This module instead names the exact slice it reads —
|
|
18
|
+
* element, document, view — so the base stays node-only, its unit test needs no DOM environment, and
|
|
19
|
+
* a real `HTMLElement` / `Window` satisfies the interfaces as-is.
|
|
20
|
+
*
|
|
21
|
+
* Why a scroll listener and not an `IntersectionObserver` (three reasons measured in mem's panel):
|
|
22
|
+
*
|
|
23
|
+
* 1. **`rootMargin` cannot prefetch here.** A plugin panel usually owns no scrollport — the host's
|
|
24
|
+
* conversation scroll body does — and an ancestor's clip rect is NOT expanded by `rootMargin`
|
|
25
|
+
* (only the intersection root's is), so the promised look-ahead never happens.
|
|
26
|
+
* 2. **An observer only reports threshold CHANGES.** One page that failed to load leaves the
|
|
27
|
+
* sentinel visible, so no new callback arrives and auto-load stays dead until something else
|
|
28
|
+
* moves it. A scroll listener is level-triggered: the next scroll event simply tries again.
|
|
29
|
+
* 3. **The scrollport is found, not assumed.** {@link scrollParentOf} walks up to whatever element
|
|
30
|
+
* really scrolls, so this keeps working if a panel ever gets its own scroller.
|
|
31
|
+
*
|
|
32
|
+
* The gap the listener alone cannot close — and why {@link AutoLoadMore.recheck} exists: a page that
|
|
33
|
+
* does NOT fill the scrollport produces no scroll event, and neither does a virtualized list that
|
|
34
|
+
* reaches its last row (the virtualizer window moves, the scroll position does not). Callers
|
|
35
|
+
* therefore re-check after a render (`recheck`), and after the data version changes
|
|
36
|
+
* (`setVersion`, which re-checks only when the number really moved).
|
|
37
|
+
*
|
|
38
|
+
* **Where the controller stops (the host boundary).** {@link scrollParentOf} has exactly one
|
|
39
|
+
* criterion for a scrollport: the element's OWN computed `overflow-y` is `auto`/`scroll` AND its
|
|
40
|
+
* `scrollHeight > clientHeight`. Two things follow, and both are deliberate rather than bugs to
|
|
41
|
+
* paper over here:
|
|
42
|
+
*
|
|
43
|
+
* - **A host whose PAGE is the scroller silently does not auto-load.** With no element satisfying the
|
|
44
|
+
* criterion, {@link createAutoLoadMore} still attaches the listener to `window` (so nothing throws,
|
|
45
|
+
* and a scrollport that appears later is picked up), but `check()` refuses to fire while it has no
|
|
46
|
+
* scroller: there is no position to read, so the `window` listener alone changes nothing. The
|
|
47
|
+
* caller then owns the gap — call {@link AutoLoadMore.recheck} from its own render/commit path, or
|
|
48
|
+
* pass a real scrollport as `target` (e.g. `document.scrollingElement`, or the element whose
|
|
49
|
+
* `scrollHeight` the page grows).
|
|
50
|
+
* - **The scrollport is resolved once, at creation.** A panel that gains (or replaces) its own
|
|
51
|
+
* scroller later must `dispose()` and recreate the controller; `check` re-probes only while it has
|
|
52
|
+
* no scroller at all.
|
|
53
|
+
*
|
|
54
|
+
* @module client/load_more
|
|
55
|
+
*/
|
|
56
|
+
/** Distance from the scrollport's end, in px, at which the next page is requested. */
|
|
57
|
+
export declare const AUTO_LOAD_MORE_MARGIN_PX = 300;
|
|
58
|
+
/**
|
|
59
|
+
* The slice of an `Element` this kit reads.
|
|
60
|
+
*
|
|
61
|
+
* A real `HTMLElement` satisfies it as-is (`scrollTop` / `scrollHeight` / `clientHeight` /
|
|
62
|
+
* `parentElement` / `ownerDocument` / the listener pair), and a test double is just an object — which
|
|
63
|
+
* is why the controller can be unit-tested without a DOM implementation.
|
|
64
|
+
*/
|
|
65
|
+
export interface ScrollElement {
|
|
66
|
+
readonly scrollTop: number;
|
|
67
|
+
readonly scrollHeight: number;
|
|
68
|
+
readonly clientHeight: number;
|
|
69
|
+
readonly parentElement: ScrollElement | null;
|
|
70
|
+
readonly ownerDocument: ScrollDocument | null;
|
|
71
|
+
addEventListener(type: 'scroll', listener: () => void, options: {
|
|
72
|
+
readonly passive: boolean;
|
|
73
|
+
}): void;
|
|
74
|
+
removeEventListener(type: 'scroll', listener: () => void): void;
|
|
75
|
+
}
|
|
76
|
+
/** The slice of a `Document` the overflow lookup reads: its view, when it has one. */
|
|
77
|
+
export interface ScrollDocument {
|
|
78
|
+
readonly defaultView: ScrollView | null;
|
|
79
|
+
}
|
|
80
|
+
/** The slice of a `Window` the overflow lookup reads. */
|
|
81
|
+
export interface ScrollView {
|
|
82
|
+
getComputedStyle(element: ScrollElement): {
|
|
83
|
+
readonly overflowY: string;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** The caller's half of the controller: what it reads, and what it does when the end is reached. */
|
|
87
|
+
export interface AutoLoadMoreOptions {
|
|
88
|
+
/**
|
|
89
|
+
* Where to start looking for the scrollport: normally the panel root (the scrollport is an
|
|
90
|
+
* ancestor — the host's conversation scroll body), or the scrollport itself when the caller owns one
|
|
91
|
+
* (a virtualized list's `getScrollElement()`). The nearest scroller at or above this node is the one
|
|
92
|
+
* that gets the listener; see {@link scrollParentOf}. `null` (the ref was not filled yet) makes the
|
|
93
|
+
* controller inert until the caller recreates it, which is what an effect that ran before the ref
|
|
94
|
+
* was set needs.
|
|
95
|
+
*/
|
|
96
|
+
readonly target: ScrollElement | null;
|
|
97
|
+
/**
|
|
98
|
+
* Read on EVERY check, never captured: a check can run long after the controller was created, and a
|
|
99
|
+
* captured `true` would keep loading pages after the caller disabled it (loading, no rows left).
|
|
100
|
+
*/
|
|
101
|
+
readonly isEnabled: () => boolean;
|
|
102
|
+
/** Asked for the next page. The caller owns the re-entrancy guard (`loading`), as with a click. */
|
|
103
|
+
readonly onLoadMore: () => void;
|
|
104
|
+
/** Initial data version (a row count). See {@link AutoLoadMore.setVersion}. */
|
|
105
|
+
readonly version?: number;
|
|
106
|
+
/** Overrides {@link AUTO_LOAD_MORE_MARGIN_PX}; a bigger value prefetches a whole row earlier. */
|
|
107
|
+
readonly marginPx?: number;
|
|
108
|
+
}
|
|
109
|
+
/** The handle `createAutoLoadMore` returns. Every method is safe to call after `dispose`. */
|
|
110
|
+
export interface AutoLoadMore {
|
|
111
|
+
/**
|
|
112
|
+
* Check the scroll position RIGHT NOW, synchronously — the explicit entry for the two cases no
|
|
113
|
+
* scroll event covers: a page that did not fill the scrollport, and a virtualized list whose range
|
|
114
|
+
* reached its last row without the scroll position moving. Call it after a render.
|
|
115
|
+
*/
|
|
116
|
+
recheck(): void;
|
|
117
|
+
/**
|
|
118
|
+
* Report a new data version (a row count, a page id). Re-checks once when the value actually
|
|
119
|
+
* changed, so a wrapper can call it on every render: the page that just landed changed the version
|
|
120
|
+
* and the next one is requested even though nothing scrolled.
|
|
121
|
+
*
|
|
122
|
+
* @returns whether the value changed — and therefore whether a check just ran. A wrapper that also
|
|
123
|
+
* has other re-check reasons (a gate reopening) can use this to run exactly ONE check per commit
|
|
124
|
+
* instead of one per reason.
|
|
125
|
+
*/
|
|
126
|
+
setVersion(version: number): boolean;
|
|
127
|
+
/** Remove the listener, cancel the queued frame, drop the callback. Idempotent. */
|
|
128
|
+
dispose(): void;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The nearest element that actually scrolls `node`, at or above it.
|
|
132
|
+
*
|
|
133
|
+
* The criterion is the computed `overflow-y` (`auto`/`scroll`) AND "it really overflows"
|
|
134
|
+
* (`scrollHeight > clientHeight`): a host's wrapper elements are `overflow-y:auto` long before their
|
|
135
|
+
* content is taller than they are, and binding to one of those would listen to an element that never
|
|
136
|
+
* emits a scroll. A `null` result is therefore honest — nothing here scrolls — and the listener falls
|
|
137
|
+
* back to `window` (see {@link createAutoLoadMore}).
|
|
138
|
+
*
|
|
139
|
+
* `node` ITSELF is considered first. A panel normally passes its root (the scrollport is an ancestor:
|
|
140
|
+
* the host's conversation scroll body), but a list that owns its scroller — a virtualized list whose
|
|
141
|
+
* `getScrollElement()` is its own container — passes that container, which is the very element
|
|
142
|
+
* scroll events fire on. Scroll events do not bubble between scrollers, so getting THIS element (not
|
|
143
|
+
* some ancestor) is what makes such a list work.
|
|
144
|
+
*/
|
|
145
|
+
export declare function scrollParentOf(node: ScrollElement | null): ScrollElement | null;
|
|
146
|
+
/**
|
|
147
|
+
* Build the controller: listen for scroll (passive + one rAF per burst), check once immediately, and
|
|
148
|
+
* stay silent while `isEnabled()` is false.
|
|
149
|
+
*
|
|
150
|
+
* The immediate first check is not an optimisation: the first page of a short list does not fill the
|
|
151
|
+
* scrollport, so waiting for a scroll event means waiting forever.
|
|
152
|
+
*
|
|
153
|
+
* @param options - see {@link AutoLoadMoreOptions}. Read at creation: `scrollParentOf` runs once, so a
|
|
154
|
+
* panel that later gets its own scroller must dispose and recreate the controller.
|
|
155
|
+
*/
|
|
156
|
+
export declare function createAutoLoadMore(options: AutoLoadMoreOptions): AutoLoadMore;
|
|
157
|
+
//# sourceMappingURL=load_more.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load_more.d.ts","sourceRoot":"","sources":["../../src/client/load_more.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,sFAAsF;AACtF,eAAO,MAAM,wBAAwB,MAAM,CAAA;AAE3C;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,aAAa,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5C,QAAQ,CAAC,aAAa,EAAE,cAAc,GAAG,IAAI,CAAA;IAC7C,gBAAgB,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAA;IACpG,mBAAmB,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;CAChE;AAED,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,WAAW,EAAE,UAAU,GAAG,IAAI,CAAA;CACxC;AAED,yDAAyD;AACzD,MAAM,WAAW,UAAU;IACzB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;CACzE;AAED,oGAAoG;AACpG,MAAM,WAAW,mBAAmB;IAClC;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IACrC;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,OAAO,CAAA;IACjC,mGAAmG;IACnG,QAAQ,CAAC,UAAU,EAAE,MAAM,IAAI,CAAA;IAC/B,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,iGAAiG;IACjG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,6FAA6F;AAC7F,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,OAAO,IAAI,IAAI,CAAA;IACf;;;;;;;;OAQG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAA;IACpC,mFAAmF;IACnF,OAAO,IAAI,IAAI,CAAA;CAChB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,GAAG,aAAa,GAAG,IAAI,CAU/E;AA0DD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAsG7E"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Scroll to the end → load the next page", as one DOM controller.
|
|
3
|
+
*
|
|
4
|
+
* **INTERNAL — no stability promise** (内部控制件,不承诺稳定): this module is reachable only through
|
|
5
|
+
* the `./internal` subpath, which the interface generation never covers (`docs/INTERFACE.md` §2/§5).
|
|
6
|
+
* It is not on `.`, it is not frozen with a generation, and a rename or a changed option shape is a
|
|
7
|
+
* base release, not a generation. Consumers (mem's 记忆 tab, mission's paged lists) follow the base.
|
|
8
|
+
*
|
|
9
|
+
* Why a DOM controller and not a React hook: this base has NO `react` peer (`package.json`), and it
|
|
10
|
+
* must stay that way — a Node user of the base must not be pulled into React, and the base ships no
|
|
11
|
+
* browser artifact. So the listener, the rAF throttle and the scrollport lookup live here, and each
|
|
12
|
+
* plugin keeps a thin wrapper of its own (mem: `useLoadMoreOnScroll`). The DOM detail then exists
|
|
13
|
+
* once instead of once per panel.
|
|
14
|
+
*
|
|
15
|
+
* Why the DOM types below are STRUCTURAL instead of `lib.dom`: the base compiles with `lib: es2023`
|
|
16
|
+
* and `types: node`, and adding `DOM` there retypes globals for the whole package (it breaks `fetch`
|
|
17
|
+
* bodies across the existing suite, for one). This module instead names the exact slice it reads —
|
|
18
|
+
* element, document, view — so the base stays node-only, its unit test needs no DOM environment, and
|
|
19
|
+
* a real `HTMLElement` / `Window` satisfies the interfaces as-is.
|
|
20
|
+
*
|
|
21
|
+
* Why a scroll listener and not an `IntersectionObserver` (three reasons measured in mem's panel):
|
|
22
|
+
*
|
|
23
|
+
* 1. **`rootMargin` cannot prefetch here.** A plugin panel usually owns no scrollport — the host's
|
|
24
|
+
* conversation scroll body does — and an ancestor's clip rect is NOT expanded by `rootMargin`
|
|
25
|
+
* (only the intersection root's is), so the promised look-ahead never happens.
|
|
26
|
+
* 2. **An observer only reports threshold CHANGES.** One page that failed to load leaves the
|
|
27
|
+
* sentinel visible, so no new callback arrives and auto-load stays dead until something else
|
|
28
|
+
* moves it. A scroll listener is level-triggered: the next scroll event simply tries again.
|
|
29
|
+
* 3. **The scrollport is found, not assumed.** {@link scrollParentOf} walks up to whatever element
|
|
30
|
+
* really scrolls, so this keeps working if a panel ever gets its own scroller.
|
|
31
|
+
*
|
|
32
|
+
* The gap the listener alone cannot close — and why {@link AutoLoadMore.recheck} exists: a page that
|
|
33
|
+
* does NOT fill the scrollport produces no scroll event, and neither does a virtualized list that
|
|
34
|
+
* reaches its last row (the virtualizer window moves, the scroll position does not). Callers
|
|
35
|
+
* therefore re-check after a render (`recheck`), and after the data version changes
|
|
36
|
+
* (`setVersion`, which re-checks only when the number really moved).
|
|
37
|
+
*
|
|
38
|
+
* **Where the controller stops (the host boundary).** {@link scrollParentOf} has exactly one
|
|
39
|
+
* criterion for a scrollport: the element's OWN computed `overflow-y` is `auto`/`scroll` AND its
|
|
40
|
+
* `scrollHeight > clientHeight`. Two things follow, and both are deliberate rather than bugs to
|
|
41
|
+
* paper over here:
|
|
42
|
+
*
|
|
43
|
+
* - **A host whose PAGE is the scroller silently does not auto-load.** With no element satisfying the
|
|
44
|
+
* criterion, {@link createAutoLoadMore} still attaches the listener to `window` (so nothing throws,
|
|
45
|
+
* and a scrollport that appears later is picked up), but `check()` refuses to fire while it has no
|
|
46
|
+
* scroller: there is no position to read, so the `window` listener alone changes nothing. The
|
|
47
|
+
* caller then owns the gap — call {@link AutoLoadMore.recheck} from its own render/commit path, or
|
|
48
|
+
* pass a real scrollport as `target` (e.g. `document.scrollingElement`, or the element whose
|
|
49
|
+
* `scrollHeight` the page grows).
|
|
50
|
+
* - **The scrollport is resolved once, at creation.** A panel that gains (or replaces) its own
|
|
51
|
+
* scroller later must `dispose()` and recreate the controller; `check` re-probes only while it has
|
|
52
|
+
* no scroller at all.
|
|
53
|
+
*
|
|
54
|
+
* @module client/load_more
|
|
55
|
+
*/
|
|
56
|
+
/** Distance from the scrollport's end, in px, at which the next page is requested. */
|
|
57
|
+
export const AUTO_LOAD_MORE_MARGIN_PX = 300;
|
|
58
|
+
/**
|
|
59
|
+
* The nearest element that actually scrolls `node`, at or above it.
|
|
60
|
+
*
|
|
61
|
+
* The criterion is the computed `overflow-y` (`auto`/`scroll`) AND "it really overflows"
|
|
62
|
+
* (`scrollHeight > clientHeight`): a host's wrapper elements are `overflow-y:auto` long before their
|
|
63
|
+
* content is taller than they are, and binding to one of those would listen to an element that never
|
|
64
|
+
* emits a scroll. A `null` result is therefore honest — nothing here scrolls — and the listener falls
|
|
65
|
+
* back to `window` (see {@link createAutoLoadMore}).
|
|
66
|
+
*
|
|
67
|
+
* `node` ITSELF is considered first. A panel normally passes its root (the scrollport is an ancestor:
|
|
68
|
+
* the host's conversation scroll body), but a list that owns its scroller — a virtualized list whose
|
|
69
|
+
* `getScrollElement()` is its own container — passes that container, which is the very element
|
|
70
|
+
* scroll events fire on. Scroll events do not bubble between scrollers, so getting THIS element (not
|
|
71
|
+
* some ancestor) is what makes such a list work.
|
|
72
|
+
*/
|
|
73
|
+
export function scrollParentOf(node) {
|
|
74
|
+
let current = node;
|
|
75
|
+
while (current !== null) {
|
|
76
|
+
const overflowY = computedOverflowY(current);
|
|
77
|
+
if ((overflowY === 'auto' || overflowY === 'scroll') && current.scrollHeight > current.clientHeight) {
|
|
78
|
+
return current;
|
|
79
|
+
}
|
|
80
|
+
current = current.parentElement;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The computed `overflow-y` of `element`, read off ITS OWN document's view.
|
|
86
|
+
*
|
|
87
|
+
* Not the bare `getComputedStyle` global: this module also runs under a test double or inside a
|
|
88
|
+
* detached document, and both need the style to come from somewhere explicit. An element with no
|
|
89
|
+
* view (no document, or a document without a window) reports `''` — "not a scrollport", the safe
|
|
90
|
+
* answer.
|
|
91
|
+
*/
|
|
92
|
+
function computedOverflowY(element) {
|
|
93
|
+
const view = element.ownerDocument?.defaultView;
|
|
94
|
+
if (view === null || view === undefined)
|
|
95
|
+
return '';
|
|
96
|
+
return view.getComputedStyle(element).overflowY;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Where to listen: the scrollport when one was found, else `window`.
|
|
100
|
+
*
|
|
101
|
+
* The `window` fallback is the case the seat's own history taught: a panel can be mounted in a
|
|
102
|
+
* document whose scrollport is the page itself. Listening there costs nothing, but the POSITION check
|
|
103
|
+
* still needs a scrollport, so nothing fires until an ancestor with its own scrollbar appears (or the
|
|
104
|
+
* caller drives {@link AutoLoadMore.recheck}). `null` only in a host without a `window` at all.
|
|
105
|
+
*/
|
|
106
|
+
function scrollEventSource(scroller) {
|
|
107
|
+
if (scroller !== null)
|
|
108
|
+
return scroller;
|
|
109
|
+
const view = globalThis['window'];
|
|
110
|
+
if (view === null || view === undefined || typeof view !== 'object')
|
|
111
|
+
return null;
|
|
112
|
+
const candidate = view;
|
|
113
|
+
if (typeof candidate.addEventListener !== 'function')
|
|
114
|
+
return null;
|
|
115
|
+
if (typeof candidate.removeEventListener !== 'function')
|
|
116
|
+
return null;
|
|
117
|
+
return view;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The host's animation-frame pair, read off `globalThis` (`lib.dom` is not in this package's types, and
|
|
121
|
+
* a node host has neither function — the controller then checks synchronously, its pre-rAF behaviour).
|
|
122
|
+
*/
|
|
123
|
+
function animationFrames() {
|
|
124
|
+
const host = globalThis;
|
|
125
|
+
const candidate = host;
|
|
126
|
+
if (typeof candidate.requestAnimationFrame !== 'function')
|
|
127
|
+
return null;
|
|
128
|
+
if (typeof candidate.cancelAnimationFrame !== 'function')
|
|
129
|
+
return null;
|
|
130
|
+
return host;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Build the controller: listen for scroll (passive + one rAF per burst), check once immediately, and
|
|
134
|
+
* stay silent while `isEnabled()` is false.
|
|
135
|
+
*
|
|
136
|
+
* The immediate first check is not an optimisation: the first page of a short list does not fill the
|
|
137
|
+
* scrollport, so waiting for a scroll event means waiting forever.
|
|
138
|
+
*
|
|
139
|
+
* @param options - see {@link AutoLoadMoreOptions}. Read at creation: `scrollParentOf` runs once, so a
|
|
140
|
+
* panel that later gets its own scroller must dispose and recreate the controller.
|
|
141
|
+
*/
|
|
142
|
+
export function createAutoLoadMore(options) {
|
|
143
|
+
const marginPx = options.marginPx ?? AUTO_LOAD_MORE_MARGIN_PX;
|
|
144
|
+
// Lazy on purpose: `scrollParentOf` only calls an element a scrollport when it is ALREADY taller
|
|
145
|
+
// than its viewport, and a controller is created before the first page of results is rendered — an
|
|
146
|
+
// empty (or short) result list answers "no scrollport" and would otherwise stay unscrollable
|
|
147
|
+
// forever. `check` re-probes and binds the moment an ancestor grows a scrollbar.
|
|
148
|
+
let scroller = scrollParentOf(options.target);
|
|
149
|
+
let source = scrollEventSource(scroller);
|
|
150
|
+
const frames = animationFrames();
|
|
151
|
+
let onLoadMore = options.onLoadMore;
|
|
152
|
+
let version = options.version ?? 0;
|
|
153
|
+
let disposed = false;
|
|
154
|
+
let queued = false;
|
|
155
|
+
let frame = null;
|
|
156
|
+
/** Bumped whenever a queued frame is superseded, so a callback the host still runs is a no-op. */
|
|
157
|
+
let generation = 0;
|
|
158
|
+
/** Move the listener onto `next`: the window fallback may be holding it, and a stale element listener
|
|
159
|
+
* would keep listening on a node that no longer scrolls. */
|
|
160
|
+
const bind = (next) => {
|
|
161
|
+
if (next === scroller)
|
|
162
|
+
return;
|
|
163
|
+
source?.removeEventListener('scroll', onScroll);
|
|
164
|
+
scroller = next;
|
|
165
|
+
source = scrollEventSource(next);
|
|
166
|
+
source?.addEventListener('scroll', onScroll, { passive: true });
|
|
167
|
+
};
|
|
168
|
+
const check = () => {
|
|
169
|
+
frame = null;
|
|
170
|
+
queued = false;
|
|
171
|
+
if (disposed || onLoadMore === null)
|
|
172
|
+
return;
|
|
173
|
+
if (scroller === null) {
|
|
174
|
+
const found = scrollParentOf(options.target);
|
|
175
|
+
if (found === null)
|
|
176
|
+
return;
|
|
177
|
+
bind(found);
|
|
178
|
+
}
|
|
179
|
+
if (scroller === null)
|
|
180
|
+
return;
|
|
181
|
+
if (!options.isEnabled())
|
|
182
|
+
return;
|
|
183
|
+
// Level-triggered: "the end is within reach" is a POSITION, not an edge, so a page that failed is
|
|
184
|
+
// retried by the next scroll event instead of waiting for a threshold crossing that never comes.
|
|
185
|
+
if (scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - marginPx)
|
|
186
|
+
onLoadMore();
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Drop the queued frame, if there is one, and make any callback already handed to the host inert.
|
|
190
|
+
*
|
|
191
|
+
* Called before a SYNC check (`recheck`/`setVersion`): the frame a scroll just scheduled reads the
|
|
192
|
+
* same position, so leaving it scheduled asks for the same page twice — the second `onLoadMore`
|
|
193
|
+
* arrives after the caller's own guard may already have cleared. The generation bump is belt and
|
|
194
|
+
* braces: a host (or a test) that runs a cancelled callback anyway must not ask again.
|
|
195
|
+
*/
|
|
196
|
+
const cancelQueued = () => {
|
|
197
|
+
if (frame !== null && frames !== null)
|
|
198
|
+
frames.cancelAnimationFrame(frame);
|
|
199
|
+
frame = null;
|
|
200
|
+
queued = false;
|
|
201
|
+
generation += 1;
|
|
202
|
+
};
|
|
203
|
+
const onScroll = () => {
|
|
204
|
+
if (disposed || queued)
|
|
205
|
+
return;
|
|
206
|
+
queued = true;
|
|
207
|
+
// One check per animation frame: a scroll burst is dozens of events, and each check may ask for a
|
|
208
|
+
// page — that must not happen dozens of times.
|
|
209
|
+
if (frames === null)
|
|
210
|
+
check();
|
|
211
|
+
else {
|
|
212
|
+
const scheduled = generation;
|
|
213
|
+
frame = frames.requestAnimationFrame(() => {
|
|
214
|
+
if (scheduled !== generation)
|
|
215
|
+
return; // superseded by a recheck/setVersion
|
|
216
|
+
check();
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
source?.addEventListener('scroll', onScroll, { passive: true });
|
|
221
|
+
check(); // the first page may leave the scrollport unfilled
|
|
222
|
+
return {
|
|
223
|
+
recheck() {
|
|
224
|
+
if (disposed)
|
|
225
|
+
return;
|
|
226
|
+
// A frame is queued from a scroll that just happened: this synchronous check supersedes it.
|
|
227
|
+
cancelQueued();
|
|
228
|
+
check();
|
|
229
|
+
},
|
|
230
|
+
setVersion(next) {
|
|
231
|
+
if (disposed || next === version)
|
|
232
|
+
return false;
|
|
233
|
+
version = next;
|
|
234
|
+
// Same supersede rule as `recheck`: one commit, one check, one possible ask.
|
|
235
|
+
cancelQueued();
|
|
236
|
+
check();
|
|
237
|
+
return true;
|
|
238
|
+
},
|
|
239
|
+
dispose() {
|
|
240
|
+
if (disposed)
|
|
241
|
+
return;
|
|
242
|
+
disposed = true;
|
|
243
|
+
onLoadMore = null;
|
|
244
|
+
source?.removeEventListener('scroll', onScroll);
|
|
245
|
+
if (frame !== null && frames !== null)
|
|
246
|
+
frames.cancelAnimationFrame(frame);
|
|
247
|
+
frame = null;
|
|
248
|
+
queued = false;
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=load_more.js.map
|
package/lib/conformance.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAIV,WAAW,EACX,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EAGf,QAAQ,EAKT,MAAM,YAAY,CAAA;AAEnB,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;AAE7D,MAAM,WAAW,gBAAgB;IAC/B,wBAAwB;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAA;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAA;CAC7C;AAED,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,4FAA4F;IAC5F,QAAQ,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,CAAA;IACxC,kFAAkF;IAClF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,yFAAyF;IACzF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,oEAAoE;IACpE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACtC,wEAAwE;IACxE,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5C,2BAA2B;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,CAAC,EAAE,WAAW,CAAA;IACzB,qEAAqE;IACrE,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAA;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAA;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,eAAe,CAAA;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,eAAe,CAAA;IACjC,gFAAgF;IAChF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,gEAAgE;IAChE,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAA;IAChC,QAAQ,CAAC,GAAG,CAAC,EAAE,cAAc,CAAA;CAC9B;
|
|
1
|
+
{"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAIV,WAAW,EACX,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EAGf,QAAQ,EAKT,MAAM,YAAY,CAAA;AAEnB,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;AAE7D,MAAM,WAAW,gBAAgB;IAC/B,wBAAwB;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAA;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAA;CAC7C;AAED,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,4FAA4F;IAC5F,QAAQ,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,CAAA;IACxC,kFAAkF;IAClF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,yFAAyF;IACzF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,oEAAoE;IACpE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACtC,wEAAwE;IACxE,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5C,2BAA2B;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,CAAC,EAAE,WAAW,CAAA;IACzB,qEAAqE;IACrE,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAA;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAA;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,eAAe,CAAA;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,eAAe,CAAA;IACjC,gFAAgF;IAChF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,gEAAgE;IAChE,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAA;IAChC,QAAQ,CAAC,GAAG,CAAC,EAAE,cAAc,CAAA;CAC9B;AAoKD,2FAA2F;AAC3F,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA4mBpG;AAED,2EAA2E;AAC3E,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAMzE"}
|
package/lib/conformance.js
CHANGED
|
@@ -54,11 +54,16 @@ function recordingFs(inner) {
|
|
|
54
54
|
const mutations = [];
|
|
55
55
|
// Recording is on by default.
|
|
56
56
|
let recording = true;
|
|
57
|
+
// Bound once: `realpath` is optional on the seam, and forwarding it only when the wrapped fs has it
|
|
58
|
+
// keeps the kit honest about the documented path-level fallback (a kit-invented realpath would hide
|
|
59
|
+
// the difference between "resolved" and "spelled"). Read-only, so nothing is recorded.
|
|
60
|
+
const realpath = inner.realpath?.bind(inner);
|
|
57
61
|
const note = (op, path, to) => {
|
|
58
62
|
if (recording)
|
|
59
63
|
mutations.push(to === undefined ? { op, path } : { op, path, to });
|
|
60
64
|
};
|
|
61
65
|
const fs = {
|
|
66
|
+
...(realpath === undefined ? {} : { realpath }),
|
|
62
67
|
readFile: path => inner.readFile(path),
|
|
63
68
|
readdir: path => inner.readdir(path),
|
|
64
69
|
stat: path => inner.stat(path),
|