@anfenn/dync 1.0.3 → 1.0.5
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 +32 -14
- package/dist/{chunk-66PSQW4D.js → chunk-YAAFAS64.js} +42 -42
- package/dist/chunk-YAAFAS64.js.map +1 -0
- package/dist/{dexie-Bv-fV10P.d.cts → dexie-1_xyU5MV.d.cts} +41 -38
- package/dist/{dexie-DJFApKsM.d.ts → dexie-ChZ0o0Sz.d.ts} +41 -38
- package/dist/dexie.cjs +40 -40
- package/dist/dexie.cjs.map +1 -1
- package/dist/dexie.d.cts +1 -1
- package/dist/dexie.d.ts +1 -1
- package/dist/dexie.js +40 -40
- package/dist/dexie.js.map +1 -1
- package/dist/index.cjs +41 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{index.shared-CkYsQYyn.d.ts → index.shared-3gbnIINY.d.ts} +4 -4
- package/dist/{index.shared-BGwvMH8f.d.cts → index.shared-XsB8HrvX.d.cts} +4 -4
- package/dist/react/index.cjs +26 -26
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +1 -1
- package/dist/wa-sqlite.cjs +2 -2
- package/dist/wa-sqlite.cjs.map +1 -1
- package/dist/wa-sqlite.d.cts +2 -2
- package/dist/wa-sqlite.d.ts +2 -2
- package/dist/wa-sqlite.js +2 -2
- package/dist/wa-sqlite.js.map +1 -1
- package/package.json +1 -1
- package/src/core/StateManager.ts +4 -4
- package/src/core/pullOperations.ts +3 -3
- package/src/core/pushOperations.ts +13 -13
- package/src/core/tableEnhancers.ts +20 -20
- package/src/helpers.ts +1 -1
- package/src/storage/dexie/DexieAdapter.ts +2 -2
- package/src/storage/dexie/{DexieStorageCollection.ts → DexieCollection.ts} +12 -12
- package/src/storage/dexie/DexieTable.ts +123 -0
- package/src/storage/dexie/{DexieStorageWhereClause.ts → DexieWhereClause.ts} +21 -21
- package/src/storage/dexie/index.ts +3 -3
- package/src/storage/memory/MemoryTable.ts +40 -40
- package/src/storage/sqlite/SQLiteTable.ts +34 -36
- package/src/storage/sqlite/drivers/WaSqliteDriver.ts +6 -6
- package/src/storage/types.ts +22 -19
- package/src/types.ts +4 -4
- package/dist/chunk-66PSQW4D.js.map +0 -1
- package/src/storage/dexie/DexieStorageTable.ts +0 -123
package/README.md
CHANGED
|
@@ -52,10 +52,11 @@ And see how Dync compares to the alternatives [below](#hasnt-this-already-been-d
|
|
|
52
52
|
...,
|
|
53
53
|
{
|
|
54
54
|
// Only add an entry here for tables that should be synced
|
|
55
|
+
// Pseudocode here, see examples for working code
|
|
55
56
|
items: {
|
|
56
57
|
add: (item) => fetch('/api/items'),
|
|
57
58
|
update: (id, changes) => fetch(`/api/items/${id}`),
|
|
58
|
-
|
|
59
|
+
delete: (id) => fetch(`/api/items/${id}`),
|
|
59
60
|
list: (since) => fetch(`/api/items?since=${since}`),
|
|
60
61
|
},
|
|
61
62
|
},
|
|
@@ -69,8 +70,21 @@ And see how Dync compares to the alternatives [below](#hasnt-this-already-been-d
|
|
|
69
70
|
...,
|
|
70
71
|
{
|
|
71
72
|
syncTables: ['items'], // Only add tables to this array that should be synced
|
|
72
|
-
push: (changes) =>
|
|
73
|
-
|
|
73
|
+
push: async (changes) => {
|
|
74
|
+
const res = await fetch('/api/sync/push', {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'Content-Type': 'application/json' },
|
|
77
|
+
body: JSON.stringify(changes),
|
|
78
|
+
});
|
|
79
|
+
return res.json();
|
|
80
|
+
},
|
|
81
|
+
pull: async (since) => {
|
|
82
|
+
const params = new URLSearchParams(
|
|
83
|
+
Object.entries(since).map(([table, date]) => [table, date.toISOString()])
|
|
84
|
+
);
|
|
85
|
+
const res = await fetch(`/api/sync/pull?${params}`);
|
|
86
|
+
return res.json();
|
|
87
|
+
},
|
|
74
88
|
},
|
|
75
89
|
);
|
|
76
90
|
```
|
|
@@ -81,8 +95,8 @@ And see how Dync compares to the alternatives [below](#hasnt-this-already-been-d
|
|
|
81
95
|
|
|
82
96
|
```ts
|
|
83
97
|
const { syncState, db } = useDync();
|
|
84
|
-
syncState.conflicts; // Record<
|
|
85
|
-
db.sync.resolveConflict(
|
|
98
|
+
syncState.conflicts; // Record<localId, Conflict>
|
|
99
|
+
db.sync.resolveConflict(localId, true);
|
|
86
100
|
```
|
|
87
101
|
|
|
88
102
|
- Optional first load data download before periodic sync is enabled
|
|
@@ -90,10 +104,14 @@ And see how Dync compares to the alternatives [below](#hasnt-this-already-been-d
|
|
|
90
104
|
- Reactive updates when data changes via `useLiveQuery()` React hook:
|
|
91
105
|
|
|
92
106
|
```ts
|
|
93
|
-
useLiveQuery(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
useLiveQuery(
|
|
108
|
+
async (db) => {
|
|
109
|
+
const items = await db.items.toArray(); // toArray() executes the query
|
|
110
|
+
setTodos(items);
|
|
111
|
+
},
|
|
112
|
+
[], // Re-run when variables change
|
|
113
|
+
['items'], // Re-run when tables change
|
|
114
|
+
);
|
|
97
115
|
```
|
|
98
116
|
|
|
99
117
|
- Sqlite schema migration
|
|
@@ -159,11 +177,11 @@ Your server records **must** have these fields. If it does but they're named dif
|
|
|
159
177
|
|
|
160
178
|
Dync auto-injects these fields into your local table schema:
|
|
161
179
|
|
|
162
|
-
| Field | Description
|
|
163
|
-
| ------------ |
|
|
164
|
-
| `_localId` | Stable local identifier, never sent to the server. Ideal for React keys.
|
|
165
|
-
| `id` | Unique identifier (any datatype). Can be assigned by client or server.
|
|
166
|
-
| `updated_at` | Assigned from the server's `updated_at` after sync. You may set it optimistically, but it's always overwritten on sync.
|
|
180
|
+
| Field | Description |
|
|
181
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
182
|
+
| `_localId` | Stable local identifier, never sent to the server. Ideal for React keys. Auto-generated UUID, but can be set manually with any unique value. |
|
|
183
|
+
| `id` | Unique identifier (any datatype). Can be assigned by client or server. |
|
|
184
|
+
| `updated_at` | Assigned from the server's `updated_at` after sync. You may set it optimistically, but it's always overwritten on sync. |
|
|
167
185
|
|
|
168
186
|
Note: `deleted` doesn't exist on the client, as it's removed during sync.
|
|
169
187
|
|
|
@@ -24,7 +24,7 @@ var UPDATED_AT = "updated_at";
|
|
|
24
24
|
var SyncAction = /* @__PURE__ */ ((SyncAction2) => {
|
|
25
25
|
SyncAction2["Create"] = "create";
|
|
26
26
|
SyncAction2["Update"] = "update";
|
|
27
|
-
SyncAction2["
|
|
27
|
+
SyncAction2["Delete"] = "delete";
|
|
28
28
|
return SyncAction2;
|
|
29
29
|
})(SyncAction || {});
|
|
30
30
|
|
|
@@ -56,7 +56,7 @@ function orderFor(a) {
|
|
|
56
56
|
return 1;
|
|
57
57
|
case "update" /* Update */:
|
|
58
58
|
return 2;
|
|
59
|
-
case "
|
|
59
|
+
case "delete" /* Delete */:
|
|
60
60
|
return 3;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -159,17 +159,17 @@ var StateManager = class {
|
|
|
159
159
|
const hasChanges = Object.keys(omittedChanges || {}).length > 0;
|
|
160
160
|
const action = change.action;
|
|
161
161
|
if (queueItem) {
|
|
162
|
-
if (queueItem.action === "
|
|
162
|
+
if (queueItem.action === "delete" /* Delete */) {
|
|
163
163
|
return Promise.resolve();
|
|
164
164
|
}
|
|
165
165
|
queueItem.version += 1;
|
|
166
|
-
if (action === "
|
|
167
|
-
queueItem.action = "
|
|
166
|
+
if (action === "delete" /* Delete */) {
|
|
167
|
+
queueItem.action = "delete" /* Delete */;
|
|
168
168
|
} else if (hasChanges) {
|
|
169
169
|
queueItem.changes = { ...queueItem.changes, ...omittedChanges };
|
|
170
170
|
queueItem.after = { ...queueItem.after, ...omittedAfter };
|
|
171
171
|
}
|
|
172
|
-
} else if (action === "
|
|
172
|
+
} else if (action === "delete" /* Delete */ || hasChanges) {
|
|
173
173
|
next.pendingChanges = [...next.pendingChanges];
|
|
174
174
|
next.pendingChanges.push({
|
|
175
175
|
action,
|
|
@@ -467,7 +467,7 @@ function enhanceSyncTable({ table, tableName, withTransaction, state, enhancedTa
|
|
|
467
467
|
if (record) {
|
|
468
468
|
deletedLocalId = record._localId;
|
|
469
469
|
await state.addPendingChange({
|
|
470
|
-
action: "
|
|
470
|
+
action: "delete" /* Delete */,
|
|
471
471
|
tableName,
|
|
472
472
|
localId: record._localId,
|
|
473
473
|
id: record.id,
|
|
@@ -481,7 +481,7 @@ function enhanceSyncTable({ table, tableName, withTransaction, state, enhancedTa
|
|
|
481
481
|
}
|
|
482
482
|
};
|
|
483
483
|
const wrappedBulkAdd = async (items) => {
|
|
484
|
-
if (items.length === 0) return;
|
|
484
|
+
if (items.length === 0) return [];
|
|
485
485
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
486
486
|
const syncedItems = items.map((item) => {
|
|
487
487
|
const localId = item._localId || createLocalId();
|
|
@@ -509,7 +509,7 @@ function enhanceSyncTable({ table, tableName, withTransaction, state, enhancedTa
|
|
|
509
509
|
return result;
|
|
510
510
|
};
|
|
511
511
|
const wrappedBulkPut = async (items) => {
|
|
512
|
-
if (items.length === 0) return;
|
|
512
|
+
if (items.length === 0) return [];
|
|
513
513
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
514
514
|
const syncedItems = items.map((item) => {
|
|
515
515
|
const localId = item._localId || createLocalId();
|
|
@@ -602,7 +602,7 @@ function enhanceSyncTable({ table, tableName, withTransaction, state, enhancedTa
|
|
|
602
602
|
if (record) {
|
|
603
603
|
deletedLocalIds.push(record._localId);
|
|
604
604
|
await state.addPendingChange({
|
|
605
|
-
action: "
|
|
605
|
+
action: "delete" /* Delete */,
|
|
606
606
|
tableName,
|
|
607
607
|
localId: record._localId,
|
|
608
608
|
id: record.id,
|
|
@@ -626,7 +626,7 @@ function enhanceSyncTable({ table, tableName, withTransaction, state, enhancedTa
|
|
|
626
626
|
if (record._localId) {
|
|
627
627
|
deletedLocalIds.push(record._localId);
|
|
628
628
|
await state.addPendingChange({
|
|
629
|
-
action: "
|
|
629
|
+
action: "delete" /* Delete */,
|
|
630
630
|
tableName,
|
|
631
631
|
localId: record._localId,
|
|
632
632
|
id: record.id,
|
|
@@ -759,20 +759,20 @@ async function processPullData(tableName, serverData, since, ctx) {
|
|
|
759
759
|
await ctx.withTransaction("rw", [tableName, DYNC_STATE_TABLE], async (tables) => {
|
|
760
760
|
const txTable = tables[tableName];
|
|
761
761
|
const pendingRemovalById = new Set(
|
|
762
|
-
ctx.state.getState().pendingChanges.filter((p) => p.tableName === tableName && p.action === "
|
|
762
|
+
ctx.state.getState().pendingChanges.filter((p) => p.tableName === tableName && p.action === "delete" /* Delete */).map((p) => p.id)
|
|
763
763
|
);
|
|
764
764
|
for (const remote of serverData) {
|
|
765
765
|
const remoteUpdated = new Date(remote.updated_at);
|
|
766
766
|
if (remoteUpdated > newest) newest = remoteUpdated;
|
|
767
767
|
if (pendingRemovalById.has(remote.id)) {
|
|
768
|
-
ctx.logger.debug(`[dync] pull:skip-pending-
|
|
768
|
+
ctx.logger.debug(`[dync] pull:skip-pending-delete tableName=${tableName} id=${remote.id}`);
|
|
769
769
|
continue;
|
|
770
770
|
}
|
|
771
771
|
const localItem = await txTable.where("id").equals(remote.id).first();
|
|
772
772
|
if (remote.deleted) {
|
|
773
773
|
if (localItem) {
|
|
774
774
|
await txTable.raw.delete(localItem._localId);
|
|
775
|
-
ctx.logger.debug(`[dync] pull:
|
|
775
|
+
ctx.logger.debug(`[dync] pull:delete tableName=${tableName} id=${remote.id}`);
|
|
776
776
|
hasChanges = true;
|
|
777
777
|
}
|
|
778
778
|
continue;
|
|
@@ -800,9 +800,9 @@ async function processPullData(tableName, serverData, since, ctx) {
|
|
|
800
800
|
}
|
|
801
801
|
|
|
802
802
|
// src/core/pushOperations.ts
|
|
803
|
-
async function
|
|
803
|
+
async function handleDeleteSuccess(change, ctx) {
|
|
804
804
|
const { tableName, localId, id } = change;
|
|
805
|
-
ctx.logger.debug(`[dync] push:
|
|
805
|
+
ctx.logger.debug(`[dync] push:delete:success tableName=${tableName} localId=${localId} id=${id}`);
|
|
806
806
|
await ctx.state.removePendingChange(localId, tableName);
|
|
807
807
|
}
|
|
808
808
|
async function handleUpdateSuccess(change, ctx) {
|
|
@@ -823,9 +823,9 @@ async function handleCreateSuccess(change, serverResult, ctx) {
|
|
|
823
823
|
if (wasChanged && ctx.state.samePendingVersion(tableName, localId, version)) {
|
|
824
824
|
await ctx.state.removePendingChange(localId, tableName);
|
|
825
825
|
} else {
|
|
826
|
-
const nextAction = wasChanged ? "update" /* Update */ : "
|
|
826
|
+
const nextAction = wasChanged ? "update" /* Update */ : "delete" /* Delete */;
|
|
827
827
|
await ctx.state.updatePendingChange(tableName, localId, nextAction, serverResult.id);
|
|
828
|
-
if (nextAction === "
|
|
828
|
+
if (nextAction === "delete" /* Delete */) return;
|
|
829
829
|
}
|
|
830
830
|
});
|
|
831
831
|
const finalItem = { ...changes, ...serverResult, _localId: localId };
|
|
@@ -850,14 +850,14 @@ async function pushOne(change, ctx) {
|
|
|
850
850
|
ctx.logger.debug(`[dync] push:attempt action=${change.action} tableName=${change.tableName} localId=${change.localId}`);
|
|
851
851
|
const { action, tableName, localId, id, changes, after } = change;
|
|
852
852
|
switch (action) {
|
|
853
|
-
case "
|
|
853
|
+
case "delete" /* Delete */:
|
|
854
854
|
if (!id) {
|
|
855
|
-
ctx.logger.warn(`[dync] push:
|
|
855
|
+
ctx.logger.warn(`[dync] push:delete:no-id tableName=${tableName} localId=${localId}`);
|
|
856
856
|
await ctx.state.removePendingChange(localId, tableName);
|
|
857
857
|
return;
|
|
858
858
|
}
|
|
859
|
-
await api.
|
|
860
|
-
await
|
|
859
|
+
await api.delete(id);
|
|
860
|
+
await handleDeleteSuccess(change, ctx);
|
|
861
861
|
break;
|
|
862
862
|
case "update" /* Update */: {
|
|
863
863
|
if (ctx.state.hasConflicts(localId)) {
|
|
@@ -953,10 +953,10 @@ async function pushAllBatch(ctx) {
|
|
|
953
953
|
}
|
|
954
954
|
const payloads = changesToPush.map((change) => ({
|
|
955
955
|
table: change.tableName,
|
|
956
|
-
action: change.action === "create" /* Create */ ? "add" : change.action === "update" /* Update */ ? "update" : "
|
|
956
|
+
action: change.action === "create" /* Create */ ? "add" : change.action === "update" /* Update */ ? "update" : "delete",
|
|
957
957
|
localId: change.localId,
|
|
958
958
|
id: change.id,
|
|
959
|
-
data: change.action === "
|
|
959
|
+
data: change.action === "delete" /* Delete */ ? void 0 : change.changes
|
|
960
960
|
}));
|
|
961
961
|
ctx.logger.debug(`[dync] push:batch:start count=${payloads.length}`);
|
|
962
962
|
const results = await ctx.batchSync.push(payloads);
|
|
@@ -994,11 +994,11 @@ async function processBatchPushResult(change, result, ctx) {
|
|
|
994
994
|
return;
|
|
995
995
|
}
|
|
996
996
|
switch (action) {
|
|
997
|
-
case "
|
|
998
|
-
|
|
997
|
+
case "delete" /* Delete */:
|
|
998
|
+
await handleDeleteSuccess(change, ctx);
|
|
999
999
|
break;
|
|
1000
1000
|
case "update" /* Update */:
|
|
1001
|
-
handleUpdateSuccess(change, ctx);
|
|
1001
|
+
await handleUpdateSuccess(change, ctx);
|
|
1002
1002
|
break;
|
|
1003
1003
|
case "create" /* Create */: {
|
|
1004
1004
|
const serverResult = { id: result.id };
|
|
@@ -2015,23 +2015,23 @@ var MemoryTable = class {
|
|
|
2015
2015
|
return this.baseBulkAdd(items);
|
|
2016
2016
|
}
|
|
2017
2017
|
baseBulkAdd(items) {
|
|
2018
|
-
|
|
2018
|
+
const keys = [];
|
|
2019
2019
|
for (let index = 0; index < items.length; index += 1) {
|
|
2020
2020
|
const item = items[index];
|
|
2021
|
-
|
|
2021
|
+
keys.push(this.baseAdd(item));
|
|
2022
2022
|
}
|
|
2023
|
-
return
|
|
2023
|
+
return keys;
|
|
2024
2024
|
}
|
|
2025
2025
|
async bulkPut(items) {
|
|
2026
2026
|
return this.baseBulkPut(items);
|
|
2027
2027
|
}
|
|
2028
2028
|
baseBulkPut(items) {
|
|
2029
|
-
|
|
2029
|
+
const keys = [];
|
|
2030
2030
|
for (let index = 0; index < items.length; index += 1) {
|
|
2031
2031
|
const item = items[index];
|
|
2032
|
-
|
|
2032
|
+
keys.push(this.basePut(item));
|
|
2033
2033
|
}
|
|
2034
|
-
return
|
|
2034
|
+
return keys;
|
|
2035
2035
|
}
|
|
2036
2036
|
async bulkGet(keys) {
|
|
2037
2037
|
return Promise.all(keys.map((key) => this.get(key)));
|
|
@@ -3251,12 +3251,12 @@ var SQLiteTable = class {
|
|
|
3251
3251
|
return this.baseBulkAdd(items);
|
|
3252
3252
|
}
|
|
3253
3253
|
async baseBulkAdd(items) {
|
|
3254
|
-
if (!items.length) return
|
|
3254
|
+
if (!items.length) return [];
|
|
3255
3255
|
const columns = this.columnNames;
|
|
3256
3256
|
const columnCount = columns.length;
|
|
3257
3257
|
const maxParamsPerBatch = 500;
|
|
3258
3258
|
const batchSize = Math.max(1, Math.floor(maxParamsPerBatch / columnCount));
|
|
3259
|
-
|
|
3259
|
+
const allKeys = [];
|
|
3260
3260
|
for (let i = 0; i < items.length; i += batchSize) {
|
|
3261
3261
|
const batch = items.slice(i, i + batchSize);
|
|
3262
3262
|
const records = batch.map((item) => this.prepareRecordForWrite(item));
|
|
@@ -3265,25 +3265,25 @@ var SQLiteTable = class {
|
|
|
3265
3265
|
const values = [];
|
|
3266
3266
|
for (const record of records) {
|
|
3267
3267
|
values.push(...this.extractColumnValues(record));
|
|
3268
|
+
allKeys.push(record[LOCAL_PK]);
|
|
3268
3269
|
}
|
|
3269
3270
|
await this.adapter.run(
|
|
3270
3271
|
`INSERT INTO ${quoteIdentifier(this.name)} (${columns.map((c) => quoteIdentifier(c)).join(", ")}) VALUES ${placeholders}`,
|
|
3271
3272
|
values
|
|
3272
3273
|
);
|
|
3273
|
-
lastKey = records[records.length - 1][LOCAL_PK];
|
|
3274
3274
|
}
|
|
3275
|
-
return
|
|
3275
|
+
return allKeys;
|
|
3276
3276
|
}
|
|
3277
3277
|
async bulkPut(items) {
|
|
3278
3278
|
return this.baseBulkPut(items);
|
|
3279
3279
|
}
|
|
3280
3280
|
async baseBulkPut(items) {
|
|
3281
|
-
if (!items.length) return
|
|
3281
|
+
if (!items.length) return [];
|
|
3282
3282
|
const columns = this.columnNames;
|
|
3283
3283
|
const columnCount = columns.length;
|
|
3284
3284
|
const maxParamsPerBatch = 500;
|
|
3285
3285
|
const batchSize = Math.max(1, Math.floor(maxParamsPerBatch / columnCount));
|
|
3286
|
-
|
|
3286
|
+
const allKeys = [];
|
|
3287
3287
|
for (let i = 0; i < items.length; i += batchSize) {
|
|
3288
3288
|
const batch = items.slice(i, i + batchSize);
|
|
3289
3289
|
const records = batch.map((item) => this.prepareRecordForWrite(item));
|
|
@@ -3292,14 +3292,14 @@ var SQLiteTable = class {
|
|
|
3292
3292
|
const values = [];
|
|
3293
3293
|
for (const record of records) {
|
|
3294
3294
|
values.push(...this.extractColumnValues(record));
|
|
3295
|
+
allKeys.push(record[LOCAL_PK]);
|
|
3295
3296
|
}
|
|
3296
3297
|
await this.adapter.run(
|
|
3297
3298
|
`INSERT OR REPLACE INTO ${quoteIdentifier(this.name)} (${columns.map((c) => quoteIdentifier(c)).join(", ")}) VALUES ${placeholders}`,
|
|
3298
3299
|
values
|
|
3299
3300
|
);
|
|
3300
|
-
lastKey = records[records.length - 1][LOCAL_PK];
|
|
3301
3301
|
}
|
|
3302
|
-
return
|
|
3302
|
+
return allKeys;
|
|
3303
3303
|
}
|
|
3304
3304
|
async bulkGet(keys) {
|
|
3305
3305
|
if (!keys.length) return [];
|
|
@@ -3881,4 +3881,4 @@ export {
|
|
|
3881
3881
|
SqliteQueryContext,
|
|
3882
3882
|
SQLiteAdapter2 as SQLiteAdapter
|
|
3883
3883
|
};
|
|
3884
|
-
//# sourceMappingURL=chunk-
|
|
3884
|
+
//# sourceMappingURL=chunk-YAAFAS64.js.map
|